@effected/yaml 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -7
- package/Yaml.js +80 -32
- package/YamlDocument.js +23 -2
- package/YamlFormat.js +162 -21
- package/YamlLint.js +239 -0
- package/YamlLintRule.js +36 -0
- package/YamlNode.js +43 -4
- package/YamlToken.js +139 -0
- package/YamlVisitor.js +52 -16
- package/index.d.ts +446 -12
- package/index.js +4 -1
- package/internal/composer/anchors.js +2 -27
- package/internal/composer/block.js +291 -67
- package/internal/composer/comments.js +217 -0
- package/internal/composer/document.js +141 -14
- package/internal/composer/flow.js +103 -10
- package/internal/composer/scalars.js +48 -11
- package/internal/composer/state.js +16 -2
- package/internal/cst-parser.js +2 -1
- package/internal/diagnostics.js +12 -3
- package/internal/fold.js +22 -10
- package/internal/rules/catalog.js +39 -0
- package/internal/rules/colon-spacing.js +77 -0
- package/internal/rules/comments-spacing.js +68 -0
- package/internal/rules/document-end.js +65 -0
- package/internal/rules/document-start.js +64 -0
- package/internal/rules/empty-lines.js +70 -0
- package/internal/rules/eof-newline.js +33 -0
- package/internal/rules/hyphen-spacing.js +52 -0
- package/internal/rules/indentation.js +103 -0
- package/internal/rules/key-duplicates.js +49 -0
- package/internal/rules/line-length.js +36 -0
- package/internal/rules/parse-validity.js +33 -0
- package/internal/rules/quoted-strings.js +81 -0
- package/internal/rules/trailing-spaces.js +39 -0
- package/internal/rules/truthy.js +84 -0
- package/internal/rules/util.js +92 -0
- package/internal/stringifier.js +342 -54
- package/package.json +3 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/rules/eof-newline.ts
|
|
6
|
+
/** Options for `eof-newline` (severity only — nothing to tune). */
|
|
7
|
+
const eofNewlineOptions = Schema.Struct({ severity: Schema.optionalKey(YamlLintSeverity) });
|
|
8
|
+
/** A missing final newline, with an inserting fix. */
|
|
9
|
+
const eofNewline = {
|
|
10
|
+
id: "eof-newline",
|
|
11
|
+
check: (ctx) => {
|
|
12
|
+
if (ctx.text.length === 0 || ctx.text.endsWith("\n")) return [];
|
|
13
|
+
const lastLine = ctx.lines[ctx.lines.length - 1];
|
|
14
|
+
if (lastLine === void 0) return [];
|
|
15
|
+
return [new YamlLintDiagnostic({
|
|
16
|
+
rule: "eof-newline",
|
|
17
|
+
severity: "error",
|
|
18
|
+
message: "No newline at end of file",
|
|
19
|
+
offset: ctx.text.length,
|
|
20
|
+
length: 0,
|
|
21
|
+
line: lastLine.number,
|
|
22
|
+
character: lastLine.text.length,
|
|
23
|
+
fix: YamlEdit.make({
|
|
24
|
+
offset: ctx.text.length,
|
|
25
|
+
length: 0,
|
|
26
|
+
content: "\n"
|
|
27
|
+
})
|
|
28
|
+
})];
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { eofNewline, eofNewlineOptions };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
|
+
import { positiveIntegerOption } from "./util.js";
|
|
4
|
+
import { Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/rules/hyphen-spacing.ts
|
|
7
|
+
/**
|
|
8
|
+
* Options for `hyphen-spacing`: `maxSpacesAfter` (default 1) after the `-`.
|
|
9
|
+
* At least one separation space must follow the indicator — `0` would make
|
|
10
|
+
* the fix emit `-item`, a plain scalar, not a sequence entry.
|
|
11
|
+
*/
|
|
12
|
+
const hyphenSpacingOptions = Schema.Struct({
|
|
13
|
+
severity: Schema.optionalKey(YamlLintSeverity),
|
|
14
|
+
maxSpacesAfter: Schema.optionalKey(positiveIntegerOption)
|
|
15
|
+
});
|
|
16
|
+
/** Spacing after the block-sequence `-` indicator. */
|
|
17
|
+
const hyphenSpacing = {
|
|
18
|
+
id: "hyphen-spacing",
|
|
19
|
+
check: (ctx, options) => {
|
|
20
|
+
const maxAfter = Math.max(1, (options ?? {}).maxSpacesAfter ?? 1);
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const token of ctx.tokens) {
|
|
23
|
+
if (token.kind !== "block-seq-entry") continue;
|
|
24
|
+
let j = token.offset + token.length;
|
|
25
|
+
let after = 0;
|
|
26
|
+
while (j < ctx.text.length && ctx.text[j] === " ") {
|
|
27
|
+
after++;
|
|
28
|
+
j++;
|
|
29
|
+
}
|
|
30
|
+
const next = ctx.text[j];
|
|
31
|
+
if (next === void 0 || next === "\n" || next === "\r" || next === "#") continue;
|
|
32
|
+
if (after > maxAfter) out.push(new YamlLintDiagnostic({
|
|
33
|
+
rule: "hyphen-spacing",
|
|
34
|
+
severity: "error",
|
|
35
|
+
message: `Too many spaces after "-" (${after} > ${maxAfter})`,
|
|
36
|
+
offset: token.offset + token.length,
|
|
37
|
+
length: after,
|
|
38
|
+
line: token.line,
|
|
39
|
+
character: token.character + token.length,
|
|
40
|
+
fix: YamlEdit.make({
|
|
41
|
+
offset: token.offset + token.length,
|
|
42
|
+
length: after - maxAfter,
|
|
43
|
+
content: ""
|
|
44
|
+
})
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { hyphenSpacing, hyphenSpacingOptions };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
2
|
+
import { coveringToken, isScalarContinuationLine, nonNegativeIntegerOption } from "./util.js";
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/rules/indentation.ts
|
|
6
|
+
/**
|
|
7
|
+
* Options for `indentation`: `spaces` per level (number or "consistent",
|
|
8
|
+
* default "consistent") and `indentSequences` (boolean or "consistent",
|
|
9
|
+
* default "consistent").
|
|
10
|
+
*/
|
|
11
|
+
const indentationOptions = Schema.Struct({
|
|
12
|
+
severity: Schema.optionalKey(YamlLintSeverity),
|
|
13
|
+
spaces: Schema.optionalKey(Schema.Union([nonNegativeIntegerOption, Schema.Literals(["consistent"])])),
|
|
14
|
+
indentSequences: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Literals(["consistent"])]))
|
|
15
|
+
});
|
|
16
|
+
/** Flow-collection depth at each line start, from the token stream. */
|
|
17
|
+
const flowDepthAtLineStarts = (ctx) => {
|
|
18
|
+
const depths = [];
|
|
19
|
+
let depth = 0;
|
|
20
|
+
let tokenIdx = 0;
|
|
21
|
+
for (const line of ctx.lines) {
|
|
22
|
+
while (tokenIdx < ctx.tokens.length) {
|
|
23
|
+
const token = ctx.tokens[tokenIdx];
|
|
24
|
+
if (token === void 0 || token.offset + token.length > line.offset) break;
|
|
25
|
+
if (token.kind === "flow-map-start" || token.kind === "flow-seq-start") depth++;
|
|
26
|
+
if (token.kind === "flow-map-end" || token.kind === "flow-seq-end") depth = Math.max(0, depth - 1);
|
|
27
|
+
tokenIdx++;
|
|
28
|
+
}
|
|
29
|
+
depths.push(depth);
|
|
30
|
+
}
|
|
31
|
+
return depths;
|
|
32
|
+
};
|
|
33
|
+
/** Block-structure indent style. */
|
|
34
|
+
const indentation = {
|
|
35
|
+
id: "indentation",
|
|
36
|
+
check: (ctx, options) => {
|
|
37
|
+
const opts = options ?? {};
|
|
38
|
+
const spacesOpt = opts.spaces ?? "consistent";
|
|
39
|
+
const seqOpt = opts.indentSequences ?? "consistent";
|
|
40
|
+
const out = [];
|
|
41
|
+
const flowDepths = flowDepthAtLineStarts(ctx);
|
|
42
|
+
const content = [];
|
|
43
|
+
for (const line of ctx.lines) {
|
|
44
|
+
const indent = line.text.length - line.text.trimStart().length;
|
|
45
|
+
const firstChar = line.text[indent];
|
|
46
|
+
if (firstChar === void 0) continue;
|
|
47
|
+
if (firstChar === "#") continue;
|
|
48
|
+
if (firstChar === "%") continue;
|
|
49
|
+
if (line.text.slice(0, indent).includes(" ")) continue;
|
|
50
|
+
if ((flowDepths[line.number] ?? 0) > 0) continue;
|
|
51
|
+
if (isScalarContinuationLine(ctx.tokens, line.offset, line.offset + indent)) continue;
|
|
52
|
+
if (line.text.slice(indent).startsWith("---") || line.text.slice(indent).startsWith("...")) continue;
|
|
53
|
+
content.push({
|
|
54
|
+
line,
|
|
55
|
+
indent,
|
|
56
|
+
firstChar
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
let unit = typeof spacesOpt === "number" ? spacesOpt : void 0;
|
|
60
|
+
const stack = [0];
|
|
61
|
+
for (const { line, indent } of content) {
|
|
62
|
+
const top = stack[stack.length - 1];
|
|
63
|
+
if (indent > top) {
|
|
64
|
+
const delta = indent - top;
|
|
65
|
+
if (unit === void 0) unit = delta;
|
|
66
|
+
else if (delta !== unit) out.push(new YamlLintDiagnostic({
|
|
67
|
+
rule: "indentation",
|
|
68
|
+
severity: "error",
|
|
69
|
+
message: `Indent of ${delta} spaces, expected ${unit}`,
|
|
70
|
+
offset: line.offset,
|
|
71
|
+
length: indent,
|
|
72
|
+
line: line.number,
|
|
73
|
+
character: 0
|
|
74
|
+
}));
|
|
75
|
+
stack.push(indent);
|
|
76
|
+
} else if (indent < top) while (stack.length > 1 && stack[stack.length - 1] > indent) stack.pop();
|
|
77
|
+
}
|
|
78
|
+
let seqIndented = typeof seqOpt === "boolean" ? seqOpt : void 0;
|
|
79
|
+
for (let i = 1; i < content.length; i++) {
|
|
80
|
+
const prev = content[i - 1];
|
|
81
|
+
const curr = content[i];
|
|
82
|
+
if (curr.firstChar !== "-") continue;
|
|
83
|
+
if (coveringToken(ctx.tokens, curr.line.offset + curr.indent)?.kind !== "block-seq-entry") continue;
|
|
84
|
+
if (!prev.line.text.replace(/ #.*$/, "").trimEnd().endsWith(":")) continue;
|
|
85
|
+
if (curr.indent !== prev.indent && curr.indent !== prev.indent + (unit ?? curr.indent - prev.indent)) continue;
|
|
86
|
+
const indented = curr.indent > prev.indent;
|
|
87
|
+
if (seqIndented === void 0) seqIndented = indented;
|
|
88
|
+
else if (indented !== seqIndented) out.push(new YamlLintDiagnostic({
|
|
89
|
+
rule: "indentation",
|
|
90
|
+
severity: "error",
|
|
91
|
+
message: seqIndented ? "Sequence under a mapping key should be indented" : "Sequence under a mapping key should not be indented",
|
|
92
|
+
offset: curr.line.offset,
|
|
93
|
+
length: curr.indent,
|
|
94
|
+
line: curr.line.number,
|
|
95
|
+
character: 0
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
export { indentation, indentationOptions };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { YamlMap, YamlScalar, YamlSeq } from "../../YamlNode.js";
|
|
2
|
+
import { keyIdentity } from "../composer/block.js";
|
|
3
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
4
|
+
import { positionAt } from "./util.js";
|
|
5
|
+
import { Schema } from "effect";
|
|
6
|
+
|
|
7
|
+
//#region src/internal/rules/key-duplicates.ts
|
|
8
|
+
/** Options for `key-duplicates` (severity only — duplicates are duplicates). */
|
|
9
|
+
const keyDuplicatesOptions = Schema.Struct({ severity: Schema.optionalKey(YamlLintSeverity) });
|
|
10
|
+
const walk = (node, text, out, ctx) => {
|
|
11
|
+
if (node === null) return;
|
|
12
|
+
if (node instanceof YamlMap) {
|
|
13
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14
|
+
for (const pair of node.items) {
|
|
15
|
+
if (pair.key instanceof YamlScalar) {
|
|
16
|
+
const id = keyIdentity(pair.key, text);
|
|
17
|
+
if (seen.has(id)) {
|
|
18
|
+
const pos = positionAt(ctx.lines, pair.key.offset);
|
|
19
|
+
out.push(new YamlLintDiagnostic({
|
|
20
|
+
rule: "key-duplicates",
|
|
21
|
+
severity: "error",
|
|
22
|
+
message: `Duplicate key: ${String(pair.key.value)}`,
|
|
23
|
+
offset: pair.key.offset,
|
|
24
|
+
length: pair.key.length,
|
|
25
|
+
line: pos.line,
|
|
26
|
+
character: pos.character
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
seen.add(id);
|
|
30
|
+
}
|
|
31
|
+
walk(pair.key, text, out, ctx);
|
|
32
|
+
walk(pair.value, text, out, ctx);
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (node instanceof YamlSeq) for (const item of node.items) walk(item, text, out, ctx);
|
|
37
|
+
};
|
|
38
|
+
/** Duplicate mapping keys anywhere in the document. */
|
|
39
|
+
const keyDuplicates = {
|
|
40
|
+
id: "key-duplicates",
|
|
41
|
+
check: (ctx) => {
|
|
42
|
+
const out = [];
|
|
43
|
+
walk(ctx.document.contents, ctx.text, out, ctx);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
export { keyDuplicates, keyDuplicatesOptions };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
2
|
+
import { nonNegativeIntegerOption } from "./util.js";
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/rules/line-length.ts
|
|
6
|
+
/**
|
|
7
|
+
* Options for `line-length`. `max` defaults to 120 — the kit-native line
|
|
8
|
+
* width (the yamllint id is recognizable; the option surface and defaults
|
|
9
|
+
* are ours).
|
|
10
|
+
*/
|
|
11
|
+
const lineLengthOptions = Schema.Struct({
|
|
12
|
+
severity: Schema.optionalKey(YamlLintSeverity),
|
|
13
|
+
max: Schema.optionalKey(nonNegativeIntegerOption)
|
|
14
|
+
});
|
|
15
|
+
const DEFAULT_MAX = 120;
|
|
16
|
+
/** Lines longer than the configured maximum. */
|
|
17
|
+
const lineLength = {
|
|
18
|
+
id: "line-length",
|
|
19
|
+
check: (ctx, options) => {
|
|
20
|
+
const max = options?.max ?? DEFAULT_MAX;
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const line of ctx.lines) if (line.text.length > max) out.push(new YamlLintDiagnostic({
|
|
23
|
+
rule: "line-length",
|
|
24
|
+
severity: "error",
|
|
25
|
+
message: `Line is longer than ${max} characters (${line.text.length})`,
|
|
26
|
+
offset: line.offset + max,
|
|
27
|
+
length: line.text.length - max,
|
|
28
|
+
line: line.number,
|
|
29
|
+
character: max
|
|
30
|
+
}));
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { lineLength, lineLengthOptions };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { YamlLintDiagnostic } from "../../YamlLintRule.js";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/rules/parse-validity.ts
|
|
5
|
+
/**
|
|
6
|
+
* parse-validity accepts no options; the config layer additionally rejects
|
|
7
|
+
* any attempt to set a severity or `"off"` on this rule.
|
|
8
|
+
*/
|
|
9
|
+
const parseValidityOptions = Schema.Struct({});
|
|
10
|
+
/** The always-on parse-validity rule. */
|
|
11
|
+
const parseValidity = {
|
|
12
|
+
id: "parse-validity",
|
|
13
|
+
check: (ctx) => [...ctx.document.errors.map((d) => new YamlLintDiagnostic({
|
|
14
|
+
rule: "parse-validity",
|
|
15
|
+
severity: "error",
|
|
16
|
+
message: d.message,
|
|
17
|
+
offset: d.offset,
|
|
18
|
+
length: d.length,
|
|
19
|
+
line: d.line,
|
|
20
|
+
character: d.character
|
|
21
|
+
})), ...ctx.document.warnings.map((d) => new YamlLintDiagnostic({
|
|
22
|
+
rule: "parse-validity",
|
|
23
|
+
severity: "warning",
|
|
24
|
+
message: d.message,
|
|
25
|
+
offset: d.offset,
|
|
26
|
+
length: d.length,
|
|
27
|
+
line: d.line,
|
|
28
|
+
character: d.character
|
|
29
|
+
}))]
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { parseValidity, parseValidityOptions };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
|
+
import { positionAt, walkScalars } from "./util.js";
|
|
4
|
+
import { Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/rules/quoted-strings.ts
|
|
7
|
+
/**
|
|
8
|
+
* Options for `quoted-strings`: the preferred `quoteType` (default
|
|
9
|
+
* `"double"`) and whether plain string scalars are `required` to be quoted
|
|
10
|
+
* at all (default `false` — only already-quoted scalars are policed).
|
|
11
|
+
*/
|
|
12
|
+
const quotedStringsOptions = Schema.Struct({
|
|
13
|
+
severity: Schema.optionalKey(YamlLintSeverity),
|
|
14
|
+
quoteType: Schema.optionalKey(Schema.Literals(["single", "double"])),
|
|
15
|
+
required: Schema.optionalKey(Schema.Boolean)
|
|
16
|
+
});
|
|
17
|
+
/** A value-preserving requote/wrap edit, or undefined when none is safe. */
|
|
18
|
+
const safeQuoteFix = (ctx, scalar, quote) => {
|
|
19
|
+
if (scalar.tag !== void 0 || scalar.anchor !== void 0) return void 0;
|
|
20
|
+
if (typeof scalar.value !== "string") return void 0;
|
|
21
|
+
const raw = ctx.text.slice(scalar.offset, scalar.offset + scalar.length);
|
|
22
|
+
if (raw.includes("\n")) return void 0;
|
|
23
|
+
const inner = scalar.style === "plain" ? raw : raw.slice(1, -1);
|
|
24
|
+
if (inner !== scalar.value) return void 0;
|
|
25
|
+
if (inner.includes(quote)) return void 0;
|
|
26
|
+
if (quote === "\"" && inner.includes("\\")) return void 0;
|
|
27
|
+
return YamlEdit.make({
|
|
28
|
+
offset: scalar.offset,
|
|
29
|
+
length: scalar.length,
|
|
30
|
+
content: `${quote}${inner}${quote}`
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
/** Quote-style policy for string value scalars. */
|
|
34
|
+
const quotedStrings = {
|
|
35
|
+
id: "quoted-strings",
|
|
36
|
+
check: (ctx, options) => {
|
|
37
|
+
const opts = options ?? {};
|
|
38
|
+
const quoteType = opts.quoteType ?? "double";
|
|
39
|
+
const required = opts.required ?? false;
|
|
40
|
+
const quote = quoteType === "double" ? "\"" : "'";
|
|
41
|
+
const wrongStyle = quoteType === "double" ? "single-quoted" : "double-quoted";
|
|
42
|
+
const out = [];
|
|
43
|
+
walkScalars(ctx.document.contents, "root", (scalar, role) => {
|
|
44
|
+
if (role === "key") return;
|
|
45
|
+
if (typeof scalar.value !== "string") return;
|
|
46
|
+
if (scalar.style === wrongStyle) {
|
|
47
|
+
const fix = safeQuoteFix(ctx, scalar, quote);
|
|
48
|
+
const pos = positionAt(ctx.lines, scalar.offset);
|
|
49
|
+
out.push(new YamlLintDiagnostic({
|
|
50
|
+
rule: "quoted-strings",
|
|
51
|
+
severity: "error",
|
|
52
|
+
message: `String should use ${quoteType} quotes`,
|
|
53
|
+
offset: scalar.offset,
|
|
54
|
+
length: scalar.length,
|
|
55
|
+
line: pos.line,
|
|
56
|
+
character: pos.character,
|
|
57
|
+
...fix !== void 0 ? { fix } : {}
|
|
58
|
+
}));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (required && scalar.style === "plain" && scalar.length > 0) {
|
|
62
|
+
const fix = safeQuoteFix(ctx, scalar, quote);
|
|
63
|
+
const pos = positionAt(ctx.lines, scalar.offset);
|
|
64
|
+
out.push(new YamlLintDiagnostic({
|
|
65
|
+
rule: "quoted-strings",
|
|
66
|
+
severity: "error",
|
|
67
|
+
message: `String should be quoted (${quoteType})`,
|
|
68
|
+
offset: scalar.offset,
|
|
69
|
+
length: scalar.length,
|
|
70
|
+
line: pos.line,
|
|
71
|
+
character: pos.character,
|
|
72
|
+
...fix !== void 0 ? { fix } : {}
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
export { quotedStrings, quotedStringsOptions };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
|
+
import { insideScalarSpan } from "./util.js";
|
|
4
|
+
import { Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/rules/trailing-spaces.ts
|
|
7
|
+
/** Options for `trailing-spaces` (severity only — nothing to tune). */
|
|
8
|
+
const trailingSpacesOptions = Schema.Struct({ severity: Schema.optionalKey(YamlLintSeverity) });
|
|
9
|
+
/** Trailing spaces or tabs at the end of a line, with a deleting fix. */
|
|
10
|
+
const trailingSpaces = {
|
|
11
|
+
id: "trailing-spaces",
|
|
12
|
+
check: (ctx) => {
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const line of ctx.lines) {
|
|
15
|
+
const match = /[ \t]+$/.exec(line.text);
|
|
16
|
+
if (match === null) continue;
|
|
17
|
+
const runOffset = line.offset + match.index;
|
|
18
|
+
if (insideScalarSpan(ctx.tokens, runOffset)) continue;
|
|
19
|
+
out.push(new YamlLintDiagnostic({
|
|
20
|
+
rule: "trailing-spaces",
|
|
21
|
+
severity: "error",
|
|
22
|
+
message: "Trailing whitespace",
|
|
23
|
+
offset: runOffset,
|
|
24
|
+
length: match[0].length,
|
|
25
|
+
line: line.number,
|
|
26
|
+
character: match.index,
|
|
27
|
+
fix: YamlEdit.make({
|
|
28
|
+
offset: runOffset,
|
|
29
|
+
length: match[0].length,
|
|
30
|
+
content: ""
|
|
31
|
+
})
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { trailingSpaces, trailingSpacesOptions };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
|
+
import { positionAt, walkScalars } from "./util.js";
|
|
4
|
+
import { Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/rules/truthy.ts
|
|
7
|
+
/**
|
|
8
|
+
* Options for `truthy`: the `allowed` boolean spellings (default
|
|
9
|
+
* `["true", "false"]`) and whether mapping keys are checked (`checkKeys`,
|
|
10
|
+
* default `true` — the workflow `on:` key is the point).
|
|
11
|
+
*/
|
|
12
|
+
const truthyOptions = Schema.Struct({
|
|
13
|
+
severity: Schema.optionalKey(YamlLintSeverity),
|
|
14
|
+
allowed: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
15
|
+
checkKeys: Schema.optionalKey(Schema.Boolean)
|
|
16
|
+
});
|
|
17
|
+
/** The YAML 1.1 boolean family, per spelling case the 1.1 grammar admits. */
|
|
18
|
+
const TRUTHY = /* @__PURE__ */ new Set([
|
|
19
|
+
"yes",
|
|
20
|
+
"Yes",
|
|
21
|
+
"YES",
|
|
22
|
+
"no",
|
|
23
|
+
"No",
|
|
24
|
+
"NO",
|
|
25
|
+
"on",
|
|
26
|
+
"On",
|
|
27
|
+
"ON",
|
|
28
|
+
"off",
|
|
29
|
+
"Off",
|
|
30
|
+
"OFF",
|
|
31
|
+
"true",
|
|
32
|
+
"True",
|
|
33
|
+
"TRUE",
|
|
34
|
+
"false",
|
|
35
|
+
"False",
|
|
36
|
+
"FALSE"
|
|
37
|
+
]);
|
|
38
|
+
const TRUE_SET = /* @__PURE__ */ new Set([
|
|
39
|
+
"yes",
|
|
40
|
+
"on",
|
|
41
|
+
"true"
|
|
42
|
+
]);
|
|
43
|
+
/** YAML 1.1 truthy spellings outside the allowed list. */
|
|
44
|
+
const truthy = {
|
|
45
|
+
id: "truthy",
|
|
46
|
+
check: (ctx, options) => {
|
|
47
|
+
const opts = options ?? {};
|
|
48
|
+
const allowed = new Set(opts.allowed ?? ["true", "false"]);
|
|
49
|
+
const checkKeys = opts.checkKeys ?? true;
|
|
50
|
+
const out = [];
|
|
51
|
+
walkScalars(ctx.document.contents, "root", (scalar, role) => {
|
|
52
|
+
if (role === "key" && !checkKeys) return;
|
|
53
|
+
if (scalar.style !== "plain" || scalar.tag !== void 0) return;
|
|
54
|
+
const raw = ctx.text.slice(scalar.offset, scalar.offset + scalar.length);
|
|
55
|
+
if (!TRUTHY.has(raw) || allowed.has(raw)) return;
|
|
56
|
+
const pos = positionAt(ctx.lines, scalar.offset);
|
|
57
|
+
const isBool = typeof scalar.value === "boolean";
|
|
58
|
+
const respell = TRUE_SET.has(raw.toLowerCase()) ? "true" : "false";
|
|
59
|
+
const fix = isBool ? allowed.has(respell) ? YamlEdit.make({
|
|
60
|
+
offset: scalar.offset,
|
|
61
|
+
length: scalar.length,
|
|
62
|
+
content: respell
|
|
63
|
+
}) : void 0 : YamlEdit.make({
|
|
64
|
+
offset: scalar.offset,
|
|
65
|
+
length: scalar.length,
|
|
66
|
+
content: `"${raw}"`
|
|
67
|
+
});
|
|
68
|
+
out.push(new YamlLintDiagnostic({
|
|
69
|
+
rule: "truthy",
|
|
70
|
+
severity: "error",
|
|
71
|
+
message: `Truthy value "${raw}" is not in the allowed spellings`,
|
|
72
|
+
offset: scalar.offset,
|
|
73
|
+
length: scalar.length,
|
|
74
|
+
line: pos.line,
|
|
75
|
+
character: pos.character,
|
|
76
|
+
...fix !== void 0 ? { fix } : {}
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
export { truthy, truthyOptions };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { YamlMap, YamlScalar, YamlSeq } from "../../YamlNode.js";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/rules/util.ts
|
|
5
|
+
/**
|
|
6
|
+
* A non-negative integer — the shape every numeric rule option takes.
|
|
7
|
+
* Rejects NaN, negatives and fractions with a message naming the constraint;
|
|
8
|
+
* the config layer's wrapper names the rule and the field.
|
|
9
|
+
*/
|
|
10
|
+
const nonNegativeIntegerOption = Schema.Number.check(Schema.makeFilter((n) => Number.isInteger(n) && n >= 0 ? void 0 : "Expected a non-negative integer"));
|
|
11
|
+
/**
|
|
12
|
+
* A positive integer — for the `maxSpacesAfter` options, where `0` would make
|
|
13
|
+
* the fix delete the separation space after an indicator and fuse it with its
|
|
14
|
+
* content (`- item` → `-item`, `a: val` → `a:val` — different tokens, not a
|
|
15
|
+
* spacing change).
|
|
16
|
+
*/
|
|
17
|
+
const positiveIntegerOption = Schema.Number.check(Schema.makeFilter((n) => Number.isInteger(n) && n >= 1 ? void 0 : "Expected an integer greater than or equal to 1"));
|
|
18
|
+
/** Depth-first walk over every scalar node with its structural role. */
|
|
19
|
+
function walkScalars(node, role, visit) {
|
|
20
|
+
if (node === null) return;
|
|
21
|
+
if (node instanceof YamlScalar) {
|
|
22
|
+
visit(node, role);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (node instanceof YamlMap) {
|
|
26
|
+
for (const pair of node.items) {
|
|
27
|
+
walkScalars(pair.key, "key", visit);
|
|
28
|
+
walkScalars(pair.value, "value", visit);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (node instanceof YamlSeq) for (const item of node.items) walkScalars(item, "item", visit);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* True when the first content of a line is the CONTINUATION of a scalar
|
|
36
|
+
* token that began on an earlier line — block-scalar bodies and multi-line
|
|
37
|
+
* plain/quoted scalars. The indentation rule skips such lines: their layout
|
|
38
|
+
* is the value's, not block structure's.
|
|
39
|
+
*/
|
|
40
|
+
function isScalarContinuationLine(tokens, lineOffset, probeOffset) {
|
|
41
|
+
const token = coveringToken(tokens, probeOffset);
|
|
42
|
+
return token !== void 0 && token.kind === "scalar" && token.offset < lineOffset;
|
|
43
|
+
}
|
|
44
|
+
/** The token whose span covers `offset`, when one does. */
|
|
45
|
+
function coveringToken(tokens, offset) {
|
|
46
|
+
let lo = 0;
|
|
47
|
+
let hi = tokens.length - 1;
|
|
48
|
+
while (lo <= hi) {
|
|
49
|
+
const mid = lo + hi >> 1;
|
|
50
|
+
const token = tokens[mid];
|
|
51
|
+
if (offset < token.offset) hi = mid - 1;
|
|
52
|
+
else if (offset >= token.offset + token.length) lo = mid + 1;
|
|
53
|
+
else return token;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* True when `offset` falls inside a scalar token's span. Layout rules use
|
|
58
|
+
* this to stay off scalar CONTENT — trailing whitespace or blank lines
|
|
59
|
+
* inside a block scalar are part of the parsed value, and a lint layer that
|
|
60
|
+
* edits content under the banner of layout is corrupting, not fixing.
|
|
61
|
+
*/
|
|
62
|
+
function insideScalarSpan(tokens, offset) {
|
|
63
|
+
return coveringToken(tokens, offset)?.kind === "scalar";
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The line containing `offset` and the character index within it — a binary
|
|
67
|
+
* search over the ordered `LintLine` array (lines are ordered by `offset`,
|
|
68
|
+
* the same invariant {@link coveringToken} rests on for tokens).
|
|
69
|
+
*/
|
|
70
|
+
function positionAt(lines, offset) {
|
|
71
|
+
let lo = 0;
|
|
72
|
+
let hi = lines.length - 1;
|
|
73
|
+
let found;
|
|
74
|
+
while (lo <= hi) {
|
|
75
|
+
const mid = lo + hi >> 1;
|
|
76
|
+
const line = lines[mid];
|
|
77
|
+
if (line.offset <= offset) {
|
|
78
|
+
found = line;
|
|
79
|
+
lo = mid + 1;
|
|
80
|
+
} else hi = mid - 1;
|
|
81
|
+
}
|
|
82
|
+
return found === void 0 ? {
|
|
83
|
+
line: 0,
|
|
84
|
+
character: 0
|
|
85
|
+
} : {
|
|
86
|
+
line: found.number,
|
|
87
|
+
character: offset - found.offset
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
export { coveringToken, insideScalarSpan, isScalarContinuationLine, nonNegativeIntegerOption, positionAt, positiveIntegerOption, walkScalars };
|