@effected/yaml 0.7.0 → 0.8.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 +78 -7
- package/Yaml.js +80 -32
- package/YamlDocument.js +22 -3
- package/YamlFormat.js +170 -19
- package/YamlLint.js +239 -0
- package/YamlLintRule.js +36 -0
- package/YamlNode.js +36 -2
- package/YamlToken.js +139 -0
- package/YamlVisitor.js +40 -5
- package/index.d.ts +439 -12
- package/index.js +4 -1
- package/internal/composer/anchors.js +2 -27
- package/internal/composer/block.js +246 -58
- package/internal/composer/comments.js +187 -0
- package/internal/composer/document.js +49 -12
- package/internal/composer/flow.js +82 -9
- 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 +266 -54
- package/package.json +3 -2
package/internal/fold.js
CHANGED
|
@@ -174,12 +174,18 @@ function renderSingleQuotedMultiline(s, indent) {
|
|
|
174
174
|
* @param explicitChomp - Original chomp indicator from the AST, when known.
|
|
175
175
|
* `keep` (`+`) and `strip` (`-`) preserve trailing-newline semantics that
|
|
176
176
|
* cannot be inferred from the resolved value alone.
|
|
177
|
+
* @param preserveKeep - Re-emit a REDUNDANT explicit `+` (a keep header whose
|
|
178
|
+
* value ends in exactly one newline, where clip would read identically) —
|
|
179
|
+
* fidelity-path header preservation; canonical mode passes false.
|
|
180
|
+
* @param explicitIndent - Explicit indentation-indicator digit from the AST,
|
|
181
|
+
* re-emitted only when it matches the rendered indent (fidelity path).
|
|
177
182
|
*/
|
|
178
|
-
function renderBlockLiteral(s, indent, explicitChomp, parentPosition) {
|
|
183
|
+
function renderBlockLiteral(s, indent, explicitChomp, parentPosition, preserveKeep = false, explicitIndent) {
|
|
179
184
|
let chomp = "";
|
|
180
185
|
const onlyNewlines = s.length > 0 && /^\n+$/.test(s);
|
|
181
186
|
if (s.endsWith("\n\n") || onlyNewlines && explicitChomp === "keep") chomp = "+";
|
|
182
187
|
else if (!s.endsWith("\n")) chomp = "-";
|
|
188
|
+
else if (preserveKeep && explicitChomp === "keep") chomp = "+";
|
|
183
189
|
const lines = s.split("\n");
|
|
184
190
|
if (s.endsWith("\n")) lines.pop();
|
|
185
191
|
let indentIndicator = "";
|
|
@@ -187,6 +193,7 @@ function renderBlockLiteral(s, indent, explicitChomp, parentPosition) {
|
|
|
187
193
|
const hasContent = firstContent !== void 0;
|
|
188
194
|
if (firstContent?.startsWith(" ") || lines.length > 0 && lines[0] === "" && hasContent) indentIndicator = String(indent.length);
|
|
189
195
|
else if (!hasContent && chomp === "+" && parentPosition === "block-map-value" && indent.length > 0) indentIndicator = String(indent.length);
|
|
196
|
+
else if (explicitIndent !== void 0 && explicitIndent === indent.length) indentIndicator = String(explicitIndent);
|
|
190
197
|
return `|${indentIndicator}${chomp}\n${lines.map((l) => l === "" ? "" : `${indent}${l}`).join("\n")}`;
|
|
191
198
|
}
|
|
192
199
|
/**
|
|
@@ -197,34 +204,39 @@ function renderBlockLiteral(s, indent, explicitChomp, parentPosition) {
|
|
|
197
204
|
* the output must contain an empty line (double newline). Each empty line
|
|
198
205
|
* in the value already produces the correct number of blank lines.
|
|
199
206
|
*/
|
|
200
|
-
function renderBlockFolded(s, indent) {
|
|
207
|
+
function renderBlockFolded(s, indent, explicitChomp, explicitIndent) {
|
|
201
208
|
let chomp = "";
|
|
202
209
|
if (s.endsWith("\n\n")) chomp = "+";
|
|
203
210
|
else if (!s.endsWith("\n")) chomp = "-";
|
|
211
|
+
else if (explicitChomp === "keep") chomp = "+";
|
|
204
212
|
const valueLines = s.split("\n");
|
|
205
213
|
if (s.endsWith("\n")) valueLines.pop();
|
|
206
214
|
let indentIndicator = "";
|
|
207
215
|
const firstContent = valueLines.find((l) => l !== "");
|
|
208
216
|
if (firstContent?.startsWith(" ") || valueLines.length >= 2 && valueLines[0] === "" && valueLines[1] === "" && firstContent !== void 0) indentIndicator = String(indent.length);
|
|
217
|
+
else if (explicitIndent !== void 0 && explicitIndent === indent.length) indentIndicator = String(explicitIndent);
|
|
209
218
|
const outputLines = [];
|
|
219
|
+
let lastContentIdx = -1;
|
|
220
|
+
for (let i = valueLines.length - 1; i >= 0; i--) if (valueLines[i] !== "") {
|
|
221
|
+
lastContentIdx = i;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
210
224
|
let prevNonEmpty = false;
|
|
211
225
|
let prevMoreIndented = false;
|
|
226
|
+
let pendingCompensation = false;
|
|
212
227
|
for (let i = 0; i < valueLines.length; i++) {
|
|
213
228
|
const line = valueLines[i];
|
|
214
229
|
if (line === "") {
|
|
215
|
-
if (prevNonEmpty && !prevMoreIndented)
|
|
216
|
-
let nextContentMoreIndented = false;
|
|
217
|
-
for (let j = i + 1; j < valueLines.length; j++) if (valueLines[j] !== "") {
|
|
218
|
-
nextContentMoreIndented = valueLines[j].startsWith(" ") || valueLines[j].startsWith(" ");
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
if (!nextContentMoreIndented) outputLines.push("");
|
|
222
|
-
}
|
|
230
|
+
if (prevNonEmpty && !prevMoreIndented && i < lastContentIdx) pendingCompensation = true;
|
|
223
231
|
outputLines.push("");
|
|
224
232
|
prevNonEmpty = false;
|
|
225
233
|
prevMoreIndented = false;
|
|
226
234
|
} else {
|
|
227
235
|
const isMoreIndented = line.startsWith(" ") || line.startsWith(" ");
|
|
236
|
+
if (pendingCompensation) {
|
|
237
|
+
if (!isMoreIndented) outputLines.push("");
|
|
238
|
+
pendingCompensation = false;
|
|
239
|
+
}
|
|
228
240
|
if (prevNonEmpty && !isMoreIndented) outputLines.push("");
|
|
229
241
|
outputLines.push(`${indent}${line}`);
|
|
230
242
|
prevNonEmpty = true;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { colonSpacing, colonSpacingOptions } from "./colon-spacing.js";
|
|
2
|
+
import { commentsSpacing, commentsSpacingOptions } from "./comments-spacing.js";
|
|
3
|
+
import { documentEnd, documentEndOptions } from "./document-end.js";
|
|
4
|
+
import { documentStart, documentStartOptions } from "./document-start.js";
|
|
5
|
+
import { emptyLines, emptyLinesOptions } from "./empty-lines.js";
|
|
6
|
+
import { eofNewline, eofNewlineOptions } from "./eof-newline.js";
|
|
7
|
+
import { hyphenSpacing, hyphenSpacingOptions } from "./hyphen-spacing.js";
|
|
8
|
+
import { indentation, indentationOptions } from "./indentation.js";
|
|
9
|
+
import { keyDuplicates, keyDuplicatesOptions } from "./key-duplicates.js";
|
|
10
|
+
import { lineLength, lineLengthOptions } from "./line-length.js";
|
|
11
|
+
import { parseValidity, parseValidityOptions } from "./parse-validity.js";
|
|
12
|
+
import { quotedStrings, quotedStringsOptions } from "./quoted-strings.js";
|
|
13
|
+
import { trailingSpaces, trailingSpacesOptions } from "./trailing-spaces.js";
|
|
14
|
+
import { truthy, truthyOptions } from "./truthy.js";
|
|
15
|
+
|
|
16
|
+
//#region src/internal/rules/catalog.ts
|
|
17
|
+
const catalog = [
|
|
18
|
+
[parseValidity, parseValidityOptions],
|
|
19
|
+
[lineLength, lineLengthOptions],
|
|
20
|
+
[trailingSpaces, trailingSpacesOptions],
|
|
21
|
+
[emptyLines, emptyLinesOptions],
|
|
22
|
+
[eofNewline, eofNewlineOptions],
|
|
23
|
+
[documentStart, documentStartOptions],
|
|
24
|
+
[documentEnd, documentEndOptions],
|
|
25
|
+
[keyDuplicates, keyDuplicatesOptions],
|
|
26
|
+
[quotedStrings, quotedStringsOptions],
|
|
27
|
+
[truthy, truthyOptions],
|
|
28
|
+
[commentsSpacing, commentsSpacingOptions],
|
|
29
|
+
[colonSpacing, colonSpacingOptions],
|
|
30
|
+
[hyphenSpacing, hyphenSpacingOptions],
|
|
31
|
+
[indentation, indentationOptions]
|
|
32
|
+
];
|
|
33
|
+
/** The built-in rules, in catalog order (parse-validity is rule `#1`). */
|
|
34
|
+
const builtinRules = catalog.map(([rule]) => rule);
|
|
35
|
+
/** Per-rule options schemas — the rule-aware half of config validation. */
|
|
36
|
+
const builtinOptionsSchemas = new Map(catalog.map(([rule, options]) => [rule.id, options]));
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { builtinOptionsSchemas, builtinRules };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
|
+
import { nonNegativeIntegerOption, positiveIntegerOption } from "./util.js";
|
|
4
|
+
import { Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/rules/colon-spacing.ts
|
|
7
|
+
/**
|
|
8
|
+
* Options for `colon-spacing`: `maxSpacesBefore` (default 0) and
|
|
9
|
+
* `maxSpacesAfter` (default 1) around the `:` indicator. `maxSpacesBefore: 0`
|
|
10
|
+
* is legal (`key:` needs no space before the colon), but at least one
|
|
11
|
+
* separation space must FOLLOW it — `0` would make the fix emit `a:val`, a
|
|
12
|
+
* plain scalar, not a mapping entry.
|
|
13
|
+
*/
|
|
14
|
+
const colonSpacingOptions = Schema.Struct({
|
|
15
|
+
severity: Schema.optionalKey(YamlLintSeverity),
|
|
16
|
+
maxSpacesBefore: Schema.optionalKey(nonNegativeIntegerOption),
|
|
17
|
+
maxSpacesAfter: Schema.optionalKey(positiveIntegerOption)
|
|
18
|
+
});
|
|
19
|
+
/** Spacing around the block-mapping `:` indicator. */
|
|
20
|
+
const colonSpacing = {
|
|
21
|
+
id: "colon-spacing",
|
|
22
|
+
check: (ctx, options) => {
|
|
23
|
+
const opts = options ?? {};
|
|
24
|
+
const maxBefore = opts.maxSpacesBefore ?? 0;
|
|
25
|
+
const maxAfter = Math.max(1, opts.maxSpacesAfter ?? 1);
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const token of ctx.tokens) {
|
|
28
|
+
if (token.kind !== "block-map-value") continue;
|
|
29
|
+
let i = token.offset - 1;
|
|
30
|
+
let before = 0;
|
|
31
|
+
while (i >= 0 && ctx.text[i] === " ") {
|
|
32
|
+
before++;
|
|
33
|
+
i--;
|
|
34
|
+
}
|
|
35
|
+
if (i >= 0 && ctx.text[i] !== "\n" && ctx.text[i] !== "\r" && ctx.text[i] !== " " && before > maxBefore) out.push(new YamlLintDiagnostic({
|
|
36
|
+
rule: "colon-spacing",
|
|
37
|
+
severity: "error",
|
|
38
|
+
message: `Too many spaces before ":" (${before} > ${maxBefore})`,
|
|
39
|
+
offset: token.offset - before,
|
|
40
|
+
length: before,
|
|
41
|
+
line: token.line,
|
|
42
|
+
character: token.character - before,
|
|
43
|
+
fix: YamlEdit.make({
|
|
44
|
+
offset: token.offset - before,
|
|
45
|
+
length: before - maxBefore,
|
|
46
|
+
content: ""
|
|
47
|
+
})
|
|
48
|
+
}));
|
|
49
|
+
let j = token.offset + token.length;
|
|
50
|
+
let after = 0;
|
|
51
|
+
while (j < ctx.text.length && ctx.text[j] === " ") {
|
|
52
|
+
after++;
|
|
53
|
+
j++;
|
|
54
|
+
}
|
|
55
|
+
const next = ctx.text[j];
|
|
56
|
+
if (next === void 0 || next === "\n" || next === "\r" || next === "#") continue;
|
|
57
|
+
if (after > maxAfter) out.push(new YamlLintDiagnostic({
|
|
58
|
+
rule: "colon-spacing",
|
|
59
|
+
severity: "error",
|
|
60
|
+
message: `Too many spaces after ":" (${after} > ${maxAfter})`,
|
|
61
|
+
offset: token.offset + token.length,
|
|
62
|
+
length: after,
|
|
63
|
+
line: token.line,
|
|
64
|
+
character: token.character + token.length,
|
|
65
|
+
fix: YamlEdit.make({
|
|
66
|
+
offset: token.offset + token.length,
|
|
67
|
+
length: after - maxAfter,
|
|
68
|
+
content: ""
|
|
69
|
+
})
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { colonSpacing, colonSpacingOptions };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
|
+
import { nonNegativeIntegerOption } from "./util.js";
|
|
4
|
+
import { Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/rules/comments-spacing.ts
|
|
7
|
+
/**
|
|
8
|
+
* Options for `comments-spacing`: `minSpacesBefore` between content and a
|
|
9
|
+
* trailing `#` (default 1 — the kit's own emission spelling) and
|
|
10
|
+
* `requireSpaceAfter` the `#` (default `true`).
|
|
11
|
+
*/
|
|
12
|
+
const commentsSpacingOptions = Schema.Struct({
|
|
13
|
+
severity: Schema.optionalKey(YamlLintSeverity),
|
|
14
|
+
minSpacesBefore: Schema.optionalKey(nonNegativeIntegerOption),
|
|
15
|
+
requireSpaceAfter: Schema.optionalKey(Schema.Boolean)
|
|
16
|
+
});
|
|
17
|
+
/** Comment spacing: after the `#`, and before a trailing comment's `#`. */
|
|
18
|
+
const commentsSpacing = {
|
|
19
|
+
id: "comments-spacing",
|
|
20
|
+
check: (ctx, options) => {
|
|
21
|
+
const opts = options ?? {};
|
|
22
|
+
const minBefore = opts.minSpacesBefore ?? 1;
|
|
23
|
+
const requireAfter = opts.requireSpaceAfter ?? true;
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const token of ctx.tokens) {
|
|
26
|
+
if (token.kind !== "comment") continue;
|
|
27
|
+
if (token.offset === 0 && token.text.startsWith("#!")) continue;
|
|
28
|
+
if (requireAfter && token.text.length > 1 && !/[ \t]/.test(token.text[1])) out.push(new YamlLintDiagnostic({
|
|
29
|
+
rule: "comments-spacing",
|
|
30
|
+
severity: "error",
|
|
31
|
+
message: "Missing space after \"#\"",
|
|
32
|
+
offset: token.offset,
|
|
33
|
+
length: 1,
|
|
34
|
+
line: token.line,
|
|
35
|
+
character: token.character,
|
|
36
|
+
fix: YamlEdit.make({
|
|
37
|
+
offset: token.offset + 1,
|
|
38
|
+
length: 0,
|
|
39
|
+
content: " "
|
|
40
|
+
})
|
|
41
|
+
}));
|
|
42
|
+
let i = token.offset - 1;
|
|
43
|
+
let spaces = 0;
|
|
44
|
+
while (i >= 0 && (ctx.text[i] === " " || ctx.text[i] === " ")) {
|
|
45
|
+
spaces++;
|
|
46
|
+
i--;
|
|
47
|
+
}
|
|
48
|
+
if (i >= 0 && ctx.text[i] !== "\n" && ctx.text[i] !== "\r" && spaces < minBefore) out.push(new YamlLintDiagnostic({
|
|
49
|
+
rule: "comments-spacing",
|
|
50
|
+
severity: "error",
|
|
51
|
+
message: `Too few spaces before comment (${spaces} < ${minBefore})`,
|
|
52
|
+
offset: token.offset - spaces,
|
|
53
|
+
length: spaces,
|
|
54
|
+
line: token.line,
|
|
55
|
+
character: token.character - spaces,
|
|
56
|
+
fix: YamlEdit.make({
|
|
57
|
+
offset: token.offset,
|
|
58
|
+
length: 0,
|
|
59
|
+
content: " ".repeat(minBefore - spaces)
|
|
60
|
+
})
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
export { commentsSpacing, commentsSpacingOptions };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/rules/document-end.ts
|
|
6
|
+
/** Options for `document-end`: require (`true`, default) or forbid the marker. */
|
|
7
|
+
const documentEndOptions = Schema.Struct({
|
|
8
|
+
severity: Schema.optionalKey(YamlLintSeverity),
|
|
9
|
+
present: Schema.optionalKey(Schema.Boolean)
|
|
10
|
+
});
|
|
11
|
+
const TRIVIA = /* @__PURE__ */ new Set([
|
|
12
|
+
"newline",
|
|
13
|
+
"whitespace",
|
|
14
|
+
"comment",
|
|
15
|
+
"byte-order-mark"
|
|
16
|
+
]);
|
|
17
|
+
/** The `...` marker at the tail of the stream. */
|
|
18
|
+
const documentEnd = {
|
|
19
|
+
id: "document-end",
|
|
20
|
+
check: (ctx, options) => {
|
|
21
|
+
const present = options?.present ?? true;
|
|
22
|
+
const tail = [...ctx.tokens].reverse().find((t) => !TRIVIA.has(t.kind));
|
|
23
|
+
const ended = tail?.kind === "document-end";
|
|
24
|
+
if (present && !ended && ctx.text.trim() !== "") {
|
|
25
|
+
const lastLine = ctx.lines[ctx.lines.length - 1];
|
|
26
|
+
const trailingNewline = ctx.text.endsWith("\n");
|
|
27
|
+
return [new YamlLintDiagnostic({
|
|
28
|
+
rule: "document-end",
|
|
29
|
+
severity: "error",
|
|
30
|
+
message: "Missing \"...\" document end marker",
|
|
31
|
+
offset: ctx.text.length,
|
|
32
|
+
length: 0,
|
|
33
|
+
line: (lastLine?.number ?? 0) + (trailingNewline ? 1 : 0),
|
|
34
|
+
character: trailingNewline ? 0 : lastLine?.text.length ?? 0,
|
|
35
|
+
fix: YamlEdit.make({
|
|
36
|
+
offset: ctx.text.length,
|
|
37
|
+
length: 0,
|
|
38
|
+
content: ctx.text.endsWith("\n") ? "...\n" : "\n...\n"
|
|
39
|
+
})
|
|
40
|
+
})];
|
|
41
|
+
}
|
|
42
|
+
if (!present && tail !== void 0 && tail.kind === "document-end") {
|
|
43
|
+
const alone = (ctx.lines[tail.line]?.text ?? "").trim() === "...";
|
|
44
|
+
const terminator = ctx.text.startsWith("\r\n", tail.offset + tail.length) ? 2 : 1;
|
|
45
|
+
return [new YamlLintDiagnostic({
|
|
46
|
+
rule: "document-end",
|
|
47
|
+
severity: "error",
|
|
48
|
+
message: "Forbidden \"...\" document end marker",
|
|
49
|
+
offset: tail.offset,
|
|
50
|
+
length: tail.length,
|
|
51
|
+
line: tail.line,
|
|
52
|
+
character: tail.character,
|
|
53
|
+
...alone ? { fix: YamlEdit.make({
|
|
54
|
+
offset: tail.offset,
|
|
55
|
+
length: tail.length + terminator,
|
|
56
|
+
content: ""
|
|
57
|
+
}) } : {}
|
|
58
|
+
})];
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
export { documentEnd, documentEndOptions };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/rules/document-start.ts
|
|
6
|
+
/** Options for `document-start`: require (`true`, default) or forbid the marker. */
|
|
7
|
+
const documentStartOptions = Schema.Struct({
|
|
8
|
+
severity: Schema.optionalKey(YamlLintSeverity),
|
|
9
|
+
present: Schema.optionalKey(Schema.Boolean)
|
|
10
|
+
});
|
|
11
|
+
const TRIVIA = /* @__PURE__ */ new Set([
|
|
12
|
+
"newline",
|
|
13
|
+
"whitespace",
|
|
14
|
+
"comment",
|
|
15
|
+
"byte-order-mark",
|
|
16
|
+
"directive"
|
|
17
|
+
]);
|
|
18
|
+
/** The `---` marker at the head of the stream. */
|
|
19
|
+
const documentStart = {
|
|
20
|
+
id: "document-start",
|
|
21
|
+
check: (ctx, options) => {
|
|
22
|
+
const present = options?.present ?? true;
|
|
23
|
+
const hasDirectives = ctx.tokens.some((t) => t.kind === "directive");
|
|
24
|
+
const first = ctx.tokens.find((t) => !TRIVIA.has(t.kind));
|
|
25
|
+
const headed = first?.kind === "document-start";
|
|
26
|
+
if (present && !headed && ctx.text.trim() !== "") return [new YamlLintDiagnostic({
|
|
27
|
+
rule: "document-start",
|
|
28
|
+
severity: "error",
|
|
29
|
+
message: "Missing \"---\" document start marker",
|
|
30
|
+
offset: 0,
|
|
31
|
+
length: 0,
|
|
32
|
+
line: 0,
|
|
33
|
+
character: 0,
|
|
34
|
+
...hasDirectives ? {} : { fix: YamlEdit.make({
|
|
35
|
+
offset: 0,
|
|
36
|
+
length: 0,
|
|
37
|
+
content: "---\n"
|
|
38
|
+
}) }
|
|
39
|
+
})];
|
|
40
|
+
if (!present && headed && first !== void 0) {
|
|
41
|
+
const lineText = ctx.lines[first.line]?.text ?? "";
|
|
42
|
+
const alone = !hasDirectives && lineText.trim() === "---";
|
|
43
|
+
const terminator = ctx.text.startsWith("\r\n", first.offset + first.length) ? 2 : 1;
|
|
44
|
+
return [new YamlLintDiagnostic({
|
|
45
|
+
rule: "document-start",
|
|
46
|
+
severity: "error",
|
|
47
|
+
message: "Forbidden \"---\" document start marker",
|
|
48
|
+
offset: first.offset,
|
|
49
|
+
length: first.length,
|
|
50
|
+
line: first.line,
|
|
51
|
+
character: first.character,
|
|
52
|
+
...alone ? { fix: YamlEdit.make({
|
|
53
|
+
offset: first.offset,
|
|
54
|
+
length: first.length + terminator,
|
|
55
|
+
content: ""
|
|
56
|
+
}) } : {}
|
|
57
|
+
})];
|
|
58
|
+
}
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { documentStart, documentStartOptions };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
+
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
|
+
import { insideScalarSpan, nonNegativeIntegerOption } from "./util.js";
|
|
4
|
+
import { Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/rules/empty-lines.ts
|
|
7
|
+
/**
|
|
8
|
+
* Options for `empty-lines`: `max` consecutive blank lines in the body
|
|
9
|
+
* (default 2), `maxStart` at the document start and `maxEnd` at the end
|
|
10
|
+
* (both default 0).
|
|
11
|
+
*/
|
|
12
|
+
const emptyLinesOptions = Schema.Struct({
|
|
13
|
+
severity: Schema.optionalKey(YamlLintSeverity),
|
|
14
|
+
max: Schema.optionalKey(nonNegativeIntegerOption),
|
|
15
|
+
maxStart: Schema.optionalKey(nonNegativeIntegerOption),
|
|
16
|
+
maxEnd: Schema.optionalKey(nonNegativeIntegerOption)
|
|
17
|
+
});
|
|
18
|
+
/** Runs of blank lines beyond the configured caps, with a deleting fix. */
|
|
19
|
+
const emptyLines = {
|
|
20
|
+
id: "empty-lines",
|
|
21
|
+
check: (ctx, options) => {
|
|
22
|
+
const opts = options ?? {};
|
|
23
|
+
const max = opts.max ?? 2;
|
|
24
|
+
const maxStart = opts.maxStart ?? 0;
|
|
25
|
+
const maxEnd = opts.maxEnd ?? 0;
|
|
26
|
+
const out = [];
|
|
27
|
+
const lines = ctx.lines;
|
|
28
|
+
let i = 0;
|
|
29
|
+
while (i < lines.length) {
|
|
30
|
+
const line = lines[i];
|
|
31
|
+
if (line === void 0 || line.text !== "" || insideScalarSpan(ctx.tokens, line.offset)) {
|
|
32
|
+
i++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
let end = i;
|
|
36
|
+
while (end < lines.length && (lines[end]?.text ?? "x") === "") end++;
|
|
37
|
+
const runLength = end - i;
|
|
38
|
+
const atStart = i === 0;
|
|
39
|
+
const atEnd = end === lines.length;
|
|
40
|
+
const allowed = atStart ? maxStart : atEnd ? maxEnd : max;
|
|
41
|
+
if (runLength > allowed) {
|
|
42
|
+
const firstExcess = lines[i + allowed];
|
|
43
|
+
const lastBlank = lines[end - 1];
|
|
44
|
+
if (firstExcess !== void 0 && lastBlank !== void 0) {
|
|
45
|
+
const nextLine = lines[end];
|
|
46
|
+
const deleteEnd = nextLine !== void 0 ? nextLine.offset : ctx.text.length;
|
|
47
|
+
out.push(new YamlLintDiagnostic({
|
|
48
|
+
rule: "empty-lines",
|
|
49
|
+
severity: "error",
|
|
50
|
+
message: `Too many consecutive blank lines (${runLength} > ${allowed})`,
|
|
51
|
+
offset: firstExcess.offset,
|
|
52
|
+
length: deleteEnd - firstExcess.offset,
|
|
53
|
+
line: firstExcess.number,
|
|
54
|
+
character: 0,
|
|
55
|
+
fix: YamlEdit.make({
|
|
56
|
+
offset: firstExcess.offset,
|
|
57
|
+
length: deleteEnd - firstExcess.offset,
|
|
58
|
+
content: ""
|
|
59
|
+
})
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
i = end;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
export { emptyLines, emptyLinesOptions };
|
|
@@ -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 };
|