@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.
Files changed (39) hide show
  1. package/README.md +78 -7
  2. package/Yaml.js +80 -32
  3. package/YamlDocument.js +22 -3
  4. package/YamlFormat.js +170 -19
  5. package/YamlLint.js +239 -0
  6. package/YamlLintRule.js +36 -0
  7. package/YamlNode.js +36 -2
  8. package/YamlToken.js +139 -0
  9. package/YamlVisitor.js +40 -5
  10. package/index.d.ts +439 -12
  11. package/index.js +4 -1
  12. package/internal/composer/anchors.js +2 -27
  13. package/internal/composer/block.js +246 -58
  14. package/internal/composer/comments.js +187 -0
  15. package/internal/composer/document.js +49 -12
  16. package/internal/composer/flow.js +82 -9
  17. package/internal/composer/scalars.js +48 -11
  18. package/internal/composer/state.js +16 -2
  19. package/internal/cst-parser.js +2 -1
  20. package/internal/diagnostics.js +12 -3
  21. package/internal/fold.js +22 -10
  22. package/internal/rules/catalog.js +39 -0
  23. package/internal/rules/colon-spacing.js +77 -0
  24. package/internal/rules/comments-spacing.js +68 -0
  25. package/internal/rules/document-end.js +65 -0
  26. package/internal/rules/document-start.js +64 -0
  27. package/internal/rules/empty-lines.js +70 -0
  28. package/internal/rules/eof-newline.js +33 -0
  29. package/internal/rules/hyphen-spacing.js +52 -0
  30. package/internal/rules/indentation.js +103 -0
  31. package/internal/rules/key-duplicates.js +49 -0
  32. package/internal/rules/line-length.js +36 -0
  33. package/internal/rules/parse-validity.js +33 -0
  34. package/internal/rules/quoted-strings.js +81 -0
  35. package/internal/rules/trailing-spaces.js +39 -0
  36. package/internal/rules/truthy.js +84 -0
  37. package/internal/rules/util.js +92 -0
  38. package/internal/stringifier.js +266 -54
  39. 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 };