@effected/yaml 0.6.1 → 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 +87 -39
- package/YamlDocument.js +25 -6
- package/YamlFormat.js +171 -20
- 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 +77 -28
- package/internal/composer/state.js +16 -2
- package/internal/cst-parser.js +11 -2
- package/internal/diagnostics.js +12 -3
- package/internal/fold.js +22 -10
- package/internal/lexer.js +23 -17
- 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 +285 -69
- package/package.json +4 -3
package/YamlFormat.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { YamlMap, YamlPair, YamlScalar, YamlSeq } from "./YamlNode.js";
|
|
2
|
-
import {
|
|
2
|
+
import { EMPTY_DOCUMENT, composeAllDocuments, composeFirstDocumentCounted } from "./internal/composer/document.js";
|
|
3
3
|
import { isFatalCode } from "./internal/diagnostics.js";
|
|
4
4
|
import { stringifyDocument, stripNodeComments } from "./internal/stringifier.js";
|
|
5
5
|
import { YamlDiagnostic } from "./YamlDiagnostic.js";
|
|
@@ -41,13 +41,20 @@ var YamlFormattingOptions = class extends Schema.Class("YamlFormattingOptions")(
|
|
|
41
41
|
}) {};
|
|
42
42
|
/**
|
|
43
43
|
* Raised when `YamlFormat.modify` cannot navigate the requested path against
|
|
44
|
-
* the composed AST (a structural mismatch)
|
|
44
|
+
* the composed AST (a structural mismatch), the source fails to parse, the
|
|
45
|
+
* source is a multi-document stream (`MultiDocumentStream` — a path names no
|
|
46
|
+
* particular document of a stream, so modify refuses rather than guessing),
|
|
47
|
+
* or the document carries `%YAML`/`%TAG` directives
|
|
48
|
+
* (`DirectiveCarryingDocument` — modify does not re-emit directive lines, and
|
|
49
|
+
* dropping a `%TAG` would orphan the shorthand tags that depend on it).
|
|
45
50
|
* Carries structured {@link YamlDiagnostic} entries — never a collapsed
|
|
46
|
-
* `reason` string (the structure-preserving-errors house rule).
|
|
51
|
+
* `reason` string (the structure-preserving-errors house rule). The error
|
|
52
|
+
* itself has no `code` field: read the code from the diagnostics —
|
|
53
|
+
* `error.diagnostics[0].code` is the primary failure.
|
|
47
54
|
*
|
|
48
55
|
* @public
|
|
49
56
|
*/
|
|
50
|
-
var YamlModificationError = class extends Schema.
|
|
57
|
+
var YamlModificationError = class extends Schema.TaggedError()("YamlModificationError", {
|
|
51
58
|
path: Schema.Array(Schema.Union([Schema.String, Schema.Number])),
|
|
52
59
|
diagnostics: Schema.Array(YamlDiagnostic)
|
|
53
60
|
}) {
|
|
@@ -97,26 +104,77 @@ function definedFields(fields) {
|
|
|
97
104
|
for (const key of Object.keys(fields)) if (fields[key] !== void 0) out[key] = fields[key];
|
|
98
105
|
return out;
|
|
99
106
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* fatal parse error (never corrupt malformed input).
|
|
104
|
-
*/
|
|
105
|
-
function formatDocument(text, options) {
|
|
106
|
-
const doc = composeFirstDocument(text, {});
|
|
107
|
-
if (doc.errors.some((e) => isFatalCode(e.code))) return void 0;
|
|
108
|
-
const preserveComments = options?.preserveComments ?? true;
|
|
109
|
-
const outputDoc = {
|
|
107
|
+
/** Rebuild a composed document for re-emission, stripping comments when asked. */
|
|
108
|
+
function toOutputDocument(doc, preserveComments) {
|
|
109
|
+
return {
|
|
110
110
|
contents: !preserveComments && doc.contents !== null ? stripNodeComments(doc.contents) : doc.contents,
|
|
111
111
|
errors: doc.errors,
|
|
112
112
|
warnings: doc.warnings,
|
|
113
113
|
directives: doc.directives,
|
|
114
|
-
...preserveComments ? definedFields({
|
|
114
|
+
...preserveComments ? definedFields({
|
|
115
|
+
comment: doc.comment,
|
|
116
|
+
commentAfter: doc.commentAfter
|
|
117
|
+
}) : {},
|
|
115
118
|
hasDocumentStart: doc.hasDocumentStart,
|
|
116
119
|
hasDocumentEnd: doc.hasDocumentEnd,
|
|
117
120
|
hasDocumentStartTab: doc.hasDocumentStartTab
|
|
118
121
|
};
|
|
119
|
-
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Format a document via the parse → stringify round-trip and diff the
|
|
125
|
+
* output against the source, returning `undefined` when the input has a
|
|
126
|
+
* fatal parse error (never corrupt malformed input) or carries
|
|
127
|
+
* `%YAML`/`%TAG` directives — `stringifyDocument` does not re-emit
|
|
128
|
+
* directive lines, and dropping a `%TAG` while keeping the shorthand tags
|
|
129
|
+
* that depend on it would turn a valid document into an unparseable one.
|
|
130
|
+
* A multi-document stream routes to {@link formatStream}, which formats
|
|
131
|
+
* every document and shares the same directive refusal.
|
|
132
|
+
*/
|
|
133
|
+
function formatDocument(text, options) {
|
|
134
|
+
const { documents, streamErrors } = composeAllDocuments(text, {});
|
|
135
|
+
if (documents.length > 1) return formatStream(documents, streamErrors, options);
|
|
136
|
+
if (streamErrors.some((e) => isFatalCode(e.code))) return void 0;
|
|
137
|
+
const doc = documents[0] ?? EMPTY_DOCUMENT;
|
|
138
|
+
if (doc.errors.some((e) => isFatalCode(e.code))) return void 0;
|
|
139
|
+
if (doc.directives.length > 0) return void 0;
|
|
140
|
+
return stringifyDocument(toOutputDocument(doc, options?.preserveComments ?? true), toStringifyInput(options));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Format a multi-document stream: every document is re-emitted in order with
|
|
144
|
+
* its own framing (`---`, `...`, leading/trailing comment blocks — all of
|
|
145
|
+
* which `stringifyDocument` renders per document), so no document is ever
|
|
146
|
+
* dropped. Receives the documents and stream errors from the ONE
|
|
147
|
+
* composition {@link formatDocument} already performed. Returns `undefined`
|
|
148
|
+
* — no edits — when the stream cannot be re-emitted faithfully:
|
|
149
|
+
*
|
|
150
|
+
* - any document (or the stream itself) carries a fatal diagnostic, the same
|
|
151
|
+
* posture as the single-document path;
|
|
152
|
+
* - any document carries `%YAML`/`%TAG` directives — `stringifyDocument`
|
|
153
|
+
* does not re-emit directive lines, and dropping a `%TAG` would change
|
|
154
|
+
* what the re-emitted document means.
|
|
155
|
+
*
|
|
156
|
+
* The single-document path ({@link formatDocument}) shares the directive
|
|
157
|
+
* refusal: re-emitting a directive-carrying document without its directive
|
|
158
|
+
* line while keeping the dependent shorthand tags produced unparseable
|
|
159
|
+
* output (probe-verified downstream against an independent oracle).
|
|
160
|
+
*/
|
|
161
|
+
function formatStream(documents, streamErrors, options) {
|
|
162
|
+
if (streamErrors.some((e) => isFatalCode(e.code))) return void 0;
|
|
163
|
+
if (documents.some((d) => d.errors.some((e) => isFatalCode(e.code)))) return void 0;
|
|
164
|
+
if (documents.some((d) => d.directives.length > 0)) return void 0;
|
|
165
|
+
const preserveComments = options?.preserveComments ?? true;
|
|
166
|
+
const base = toStringifyInput(options);
|
|
167
|
+
const parts = [];
|
|
168
|
+
for (let i = 0; i < documents.length; i++) {
|
|
169
|
+
const doc = documents[i];
|
|
170
|
+
if (i > 0 && !doc.hasDocumentStart && !documents[i - 1].hasDocumentEnd) return void 0;
|
|
171
|
+
const docOptions = i < documents.length - 1 ? {
|
|
172
|
+
...base,
|
|
173
|
+
finalNewline: true
|
|
174
|
+
} : base;
|
|
175
|
+
parts.push(stringifyDocument(toOutputDocument(doc, preserveComments), docOptions));
|
|
176
|
+
}
|
|
177
|
+
return parts.join("");
|
|
120
178
|
}
|
|
121
179
|
/** Convert a plain JS scalar value into a synthetic `YamlScalar` (offset/length are irrelevant — immediately re-stringified). */
|
|
122
180
|
function jsValueToNode(value) {
|
|
@@ -151,7 +209,11 @@ function modifyNode(node, path, depth, value) {
|
|
|
151
209
|
newItems[pairIndex] = YamlPair.make({
|
|
152
210
|
key: oldPair.key,
|
|
153
211
|
value: newValueNode,
|
|
154
|
-
...definedFields({
|
|
212
|
+
...definedFields({
|
|
213
|
+
commentBefore: oldPair.commentBefore,
|
|
214
|
+
comment: oldPair.comment,
|
|
215
|
+
spaceBefore: oldPair.spaceBefore
|
|
216
|
+
})
|
|
155
217
|
});
|
|
156
218
|
return rebuildMap(node, newItems);
|
|
157
219
|
}
|
|
@@ -175,7 +237,11 @@ function modifyNode(node, path, depth, value) {
|
|
|
175
237
|
newItems[pairIndex] = YamlPair.make({
|
|
176
238
|
key: pair.key,
|
|
177
239
|
value: newValue,
|
|
178
|
-
...definedFields({
|
|
240
|
+
...definedFields({
|
|
241
|
+
commentBefore: pair.commentBefore,
|
|
242
|
+
comment: pair.comment,
|
|
243
|
+
spaceBefore: pair.spaceBefore
|
|
244
|
+
})
|
|
179
245
|
});
|
|
180
246
|
return rebuildMap(node, newItems);
|
|
181
247
|
}
|
|
@@ -206,7 +272,9 @@ function rebuildMap(node, items) {
|
|
|
206
272
|
...definedFields({
|
|
207
273
|
tag: node.tag,
|
|
208
274
|
anchor: node.anchor,
|
|
275
|
+
commentBefore: node.commentBefore,
|
|
209
276
|
comment: node.comment,
|
|
277
|
+
spaceBefore: node.spaceBefore,
|
|
210
278
|
sourceMultiline: node.sourceMultiline
|
|
211
279
|
}),
|
|
212
280
|
offset: node.offset,
|
|
@@ -220,7 +288,9 @@ function rebuildSeq(node, items) {
|
|
|
220
288
|
...definedFields({
|
|
221
289
|
tag: node.tag,
|
|
222
290
|
anchor: node.anchor,
|
|
291
|
+
commentBefore: node.commentBefore,
|
|
223
292
|
comment: node.comment,
|
|
293
|
+
spaceBefore: node.spaceBefore,
|
|
224
294
|
sourceMultiline: node.sourceMultiline
|
|
225
295
|
}),
|
|
226
296
|
offset: node.offset,
|
|
@@ -239,6 +309,24 @@ function rebuildSeq(node, items) {
|
|
|
239
309
|
* `diagnostics: ReadonlyArray<YamlDiagnostic>`, never a collapsed `reason`
|
|
240
310
|
* string.
|
|
241
311
|
*
|
|
312
|
+
* `format`/`formatToString` handle multi-document streams whole — every
|
|
313
|
+
* document is re-emitted in order with its own framing, never a silently
|
|
314
|
+
* truncated stream. `modify`/`modifyToString` are **single-document**: a
|
|
315
|
+
* {@link YamlPath} carries no document index, so which document of a stream
|
|
316
|
+
* a path names is a rule the format does not define, and a multi-document
|
|
317
|
+
* stream fails typed with a `MultiDocumentStream` diagnostic rather than
|
|
318
|
+
* guessing. Parse a stream with {@link Yaml.parseAll} /
|
|
319
|
+
* `Yaml.parseAllResult`.
|
|
320
|
+
*
|
|
321
|
+
* **Directive-carrying input is refused on every path.** The stringifier does
|
|
322
|
+
* not re-emit `%YAML`/`%TAG` directive lines, and re-emitting a document
|
|
323
|
+
* without its `%TAG` while keeping the shorthand tags that depend on it
|
|
324
|
+
* (`!e!foo`) turns a valid file into one no parser can read. So
|
|
325
|
+
* `format`/`formatToString` leave such input byte-identical (no edits), and
|
|
326
|
+
* `modify`/`modifyToString` fail typed with a `DirectiveCarryingDocument`
|
|
327
|
+
* diagnostic. Directive re-emission is unimplemented, not undesired — the
|
|
328
|
+
* refusal is the floor that stops corruption until it lands.
|
|
329
|
+
*
|
|
242
330
|
* @public
|
|
243
331
|
*/
|
|
244
332
|
var YamlFormat = class YamlFormat {
|
|
@@ -249,6 +337,24 @@ var YamlFormat = class YamlFormat {
|
|
|
249
337
|
* Pure and total: malformed input (a fatal parse error) yields `[]` rather
|
|
250
338
|
* than corrupting the document.
|
|
251
339
|
*
|
|
340
|
+
* **Multi-document streams format whole.** Input containing more than one
|
|
341
|
+
* document (a `---`-separated stream — a Kubernetes manifest, a pnpm 11
|
|
342
|
+
* `pnpm-lock.yaml` with a config-dependency preamble) formats every
|
|
343
|
+
* document in order, re-emitting each document's own framing (`---`,
|
|
344
|
+
* `...`, comment blocks); no document is ever dropped. Document detection
|
|
345
|
+
* is CST-level, so a `---` inside a block scalar or quoted string is
|
|
346
|
+
* content, not a document boundary. Two multi-document shapes yield `[]`
|
|
347
|
+
* (untouched) because they cannot be re-emitted faithfully: a stream with
|
|
348
|
+
* a fatal diagnostic in any document (the same posture as single-document
|
|
349
|
+
* input) and a stream carrying `%YAML`/`%TAG` directives.
|
|
350
|
+
*
|
|
351
|
+
* **Directive-carrying documents yield `[]` on the single-document path
|
|
352
|
+
* too.** Directive lines are not re-emitted, and dropping a `%TAG` while
|
|
353
|
+
* keeping the shorthand tags that depend on it would turn a valid document
|
|
354
|
+
* into an unparseable one — so a document carrying `%YAML`/`%TAG`
|
|
355
|
+
* directives is left untouched. Detection is directive-token-level: a
|
|
356
|
+
* literal `%TAG` inside a scalar is content and formats normally.
|
|
357
|
+
*
|
|
252
358
|
* @remarks
|
|
253
359
|
* The positional `range` argument takes precedence over
|
|
254
360
|
* `options?.range` when both are given; either accepts a plain
|
|
@@ -278,6 +384,13 @@ var YamlFormat = class YamlFormat {
|
|
|
278
384
|
/**
|
|
279
385
|
* Format `text` and apply the resulting edits in one step
|
|
280
386
|
* (`YamlEdit.applyAll ∘ format`). Pure and total.
|
|
387
|
+
*
|
|
388
|
+
* Inherits the {@link YamlFormat.format} contract: a multi-document stream
|
|
389
|
+
* is formatted whole — every document re-emitted in order — and input
|
|
390
|
+
* that cannot be formatted faithfully (a fatal parse error, or any
|
|
391
|
+
* document — single or in a stream — carrying `%YAML`/`%TAG` directives)
|
|
392
|
+
* is returned byte-identical — never a truncated first document, never a
|
|
393
|
+
* document re-emitted without the directive its tags depend on.
|
|
281
394
|
*/
|
|
282
395
|
static formatToString(text, range, options) {
|
|
283
396
|
return YamlEdit.applyAll(text, YamlFormat.format(text, range, options));
|
|
@@ -291,18 +404,53 @@ var YamlFormat = class YamlFormat {
|
|
|
291
404
|
* {@link YamlModificationError} on a fatal parse error or a structural
|
|
292
405
|
* navigation mismatch.
|
|
293
406
|
*
|
|
407
|
+
* **Single-document contract.** A `path` carries no document index, so on
|
|
408
|
+
* a multi-document stream there is no rule for which document it names —
|
|
409
|
+
* `modify` fails with {@link YamlModificationError} carrying a
|
|
410
|
+
* `MultiDocumentStream` diagnostic rather than guessing document 1 (and
|
|
411
|
+
* unlike {@link YamlFormat.format}, which formats a stream whole because
|
|
412
|
+
* formatting needs no target). Detection is CST-level: a `---` inside a
|
|
413
|
+
* block scalar or quoted string is content, not a document boundary.
|
|
414
|
+
*
|
|
415
|
+
* **Directive-carrying documents are refused.** A document carrying
|
|
416
|
+
* `%YAML`/`%TAG` directives fails with {@link YamlModificationError}
|
|
417
|
+
* carrying a `DirectiveCarryingDocument` diagnostic: modify re-emits the
|
|
418
|
+
* whole document and does not re-emit directive lines, so applying it
|
|
419
|
+
* would drop the `%TAG` while keeping the shorthand tags that depend on
|
|
420
|
+
* it — unparseable output. A typed refusal beats silent corruption;
|
|
421
|
+
* directive re-emission is unimplemented, not undesired. A literal
|
|
422
|
+
* `%TAG` inside a scalar is content and does not trigger the refusal.
|
|
423
|
+
*
|
|
294
424
|
* @remarks
|
|
295
425
|
* `options` is a bare {@link YamlStringifyOptions} — it controls only the
|
|
296
426
|
* internal re-stringify step, not a range (there is no range to restrict
|
|
297
427
|
* for a path-targeted modification).
|
|
298
428
|
*/
|
|
299
429
|
static modify = Effect.fn("YamlFormat.modify")(function* (text, path, value, options) {
|
|
300
|
-
const doc =
|
|
430
|
+
const { document: doc, documentCount } = composeFirstDocumentCounted(text, {});
|
|
431
|
+
if (documentCount > 1) return yield* new YamlModificationError({
|
|
432
|
+
path,
|
|
433
|
+
diagnostics: [YamlDiagnostic.fromRaw({
|
|
434
|
+
code: "MultiDocumentStream",
|
|
435
|
+
message: `Cannot modify a multi-document stream (${documentCount} documents): modify re-emits exactly one document`,
|
|
436
|
+
offset: 0,
|
|
437
|
+
length: 0
|
|
438
|
+
}, text)]
|
|
439
|
+
});
|
|
301
440
|
const fatal = doc.errors.filter((e) => isFatalCode(e.code));
|
|
302
441
|
if (fatal.length > 0) return yield* new YamlModificationError({
|
|
303
442
|
path,
|
|
304
443
|
diagnostics: fatal.map((e) => YamlDiagnostic.fromRaw(e, text))
|
|
305
444
|
});
|
|
445
|
+
if (doc.directives.length > 0) return yield* new YamlModificationError({
|
|
446
|
+
path,
|
|
447
|
+
diagnostics: [YamlDiagnostic.fromRaw({
|
|
448
|
+
code: "DirectiveCarryingDocument",
|
|
449
|
+
message: "Cannot modify a document carrying %YAML/%TAG directives: modify does not re-emit directive lines, and dropping a %TAG orphans the shorthand tags that depend on it",
|
|
450
|
+
offset: 0,
|
|
451
|
+
length: 0
|
|
452
|
+
}, text)]
|
|
453
|
+
});
|
|
306
454
|
let newContents;
|
|
307
455
|
try {
|
|
308
456
|
newContents = modifyDocument(doc, path, value);
|
|
@@ -327,7 +475,10 @@ var YamlFormat = class YamlFormat {
|
|
|
327
475
|
});
|
|
328
476
|
/**
|
|
329
477
|
* Modify `text` and apply the resulting edits in one step
|
|
330
|
-
* (`YamlEdit.applyAll ∘ modify`).
|
|
478
|
+
* (`YamlEdit.applyAll ∘ modify`). Inherits the {@link YamlFormat.modify}
|
|
479
|
+
* error channel, including the single-document contract's
|
|
480
|
+
* `MultiDocumentStream` refusal and the `DirectiveCarryingDocument`
|
|
481
|
+
* refusal of `%YAML`/`%TAG`-carrying documents.
|
|
331
482
|
*/
|
|
332
483
|
static modifyToString = Effect.fn("YamlFormat.modifyToString")(function* (text, path, value, options) {
|
|
333
484
|
const edits = yield* YamlFormat.modify(text, path, value, options);
|
package/YamlLint.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { composeFirstDocument } from "./internal/composer/document.js";
|
|
2
|
+
import { isFatalCode } from "./internal/diagnostics.js";
|
|
3
|
+
import { YamlParseError } from "./Yaml.js";
|
|
4
|
+
import { documentFromRaw } from "./YamlDocument.js";
|
|
5
|
+
import { YamlEdit } from "./YamlEdit.js";
|
|
6
|
+
import { YamlLintDiagnostic } from "./YamlLintRule.js";
|
|
7
|
+
import { builtinOptionsSchemas, builtinRules } from "./internal/rules/catalog.js";
|
|
8
|
+
import { YamlTokens } from "./YamlToken.js";
|
|
9
|
+
import { Result, Schema } from "effect";
|
|
10
|
+
|
|
11
|
+
//#region src/YamlLint.ts
|
|
12
|
+
/**
|
|
13
|
+
* One entry of the config `rules` map: a bare severity literal (the common
|
|
14
|
+
* case), `"off"` to disable, or a typed per-rule options object (the tuning
|
|
15
|
+
* case; may carry its own `severity`).
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
const YamlLintRuleSetting = Schema.Union([Schema.Literals([
|
|
20
|
+
"error",
|
|
21
|
+
"warning",
|
|
22
|
+
"off"
|
|
23
|
+
]), Schema.Record(Schema.String, Schema.Unknown)]);
|
|
24
|
+
/** Rule-aware validation of the config `rules` map (undefined = valid). */
|
|
25
|
+
const validateRulesMap = (rules) => {
|
|
26
|
+
for (const [id, entry] of Object.entries(rules)) {
|
|
27
|
+
if (id === "parse-validity") {
|
|
28
|
+
if (entry === "off" || entry === "warning") return `Rule "parse-validity" is always-on and cannot be set to "${entry}"`;
|
|
29
|
+
if (typeof entry === "object") return `Rule "parse-validity" accepts no options`;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (typeof entry === "object") {
|
|
33
|
+
const optionsSchema = builtinOptionsSchemas.get(id);
|
|
34
|
+
if (optionsSchema !== void 0) {
|
|
35
|
+
const decoded = Schema.decodeUnknownResult(optionsSchema, { onExcessProperty: "error" })(entry);
|
|
36
|
+
if (Result.isFailure(decoded)) return `Invalid options for rule "${id}": ${String(decoded.failure)}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The lint configuration: a `rules` map keying rule ids (built-in or custom)
|
|
43
|
+
* to a severity literal or a typed per-rule options object.
|
|
44
|
+
*
|
|
45
|
+
* Validation is rule-aware for the built-in catalog — a mistyped option on a
|
|
46
|
+
* built-in rule fails schema validation with a typed error naming the rule,
|
|
47
|
+
* and any attempt to demote or disable the always-on `parse-validity` rule
|
|
48
|
+
* is rejected. Custom rule ids are accepted with a bare severity or an
|
|
49
|
+
* opaque options object the custom rule validates itself.
|
|
50
|
+
*
|
|
51
|
+
* Presets ship as the statics {@link YamlLintConfig.default} and
|
|
52
|
+
* {@link YamlLintConfig.relaxed} — composing one is object spread over a
|
|
53
|
+
* static, not an `extends` string resolution step (this package owns no
|
|
54
|
+
* config-file loader).
|
|
55
|
+
*
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
var YamlLintConfig = class YamlLintConfig extends Schema.Class("YamlLintConfig")({ rules: Schema.Record(Schema.String, YamlLintRuleSetting).pipe(Schema.check(Schema.makeFilter(validateRulesMap))) }) {
|
|
59
|
+
/**
|
|
60
|
+
* The default preset. Rule entries accrete as built-ins land; the
|
|
61
|
+
* `quoted-strings` rule defaults to DOUBLE quotes here (the one taste
|
|
62
|
+
* call the design pins).
|
|
63
|
+
*/
|
|
64
|
+
static default = YamlLintConfig.make({ rules: {
|
|
65
|
+
"line-length": "error",
|
|
66
|
+
"trailing-spaces": "error",
|
|
67
|
+
"empty-lines": "error",
|
|
68
|
+
"eof-newline": "error",
|
|
69
|
+
"key-duplicates": "error",
|
|
70
|
+
"quoted-strings": { quoteType: "double" },
|
|
71
|
+
truthy: "error",
|
|
72
|
+
"comments-spacing": "error",
|
|
73
|
+
"colon-spacing": "error",
|
|
74
|
+
"hyphen-spacing": "error",
|
|
75
|
+
indentation: "error"
|
|
76
|
+
} });
|
|
77
|
+
/** The relaxed preset: style rules demoted to warnings. */
|
|
78
|
+
static relaxed = YamlLintConfig.make({ rules: {
|
|
79
|
+
"line-length": "warning",
|
|
80
|
+
"trailing-spaces": "warning",
|
|
81
|
+
"empty-lines": "warning",
|
|
82
|
+
"eof-newline": "warning",
|
|
83
|
+
"key-duplicates": "error",
|
|
84
|
+
"quoted-strings": {
|
|
85
|
+
quoteType: "double",
|
|
86
|
+
severity: "warning"
|
|
87
|
+
},
|
|
88
|
+
truthy: "warning",
|
|
89
|
+
"comments-spacing": "warning",
|
|
90
|
+
"colon-spacing": "warning",
|
|
91
|
+
"hyphen-spacing": "warning",
|
|
92
|
+
indentation: "warning"
|
|
93
|
+
} });
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Split `text` into positioned lines. Line text excludes the terminator
|
|
97
|
+
* whole — for CRLF input the `\r` is part of the terminator and is stripped
|
|
98
|
+
* too, so every rule sees the same line content regardless of line-ending
|
|
99
|
+
* style. Empty input has no lines: rules that count blank lines must not see
|
|
100
|
+
* a phantom first line.
|
|
101
|
+
*/
|
|
102
|
+
const buildLines = (text) => {
|
|
103
|
+
if (text === "") return [];
|
|
104
|
+
const lines = [];
|
|
105
|
+
let offset = 0;
|
|
106
|
+
let number = 0;
|
|
107
|
+
while (offset <= text.length) {
|
|
108
|
+
const nl = text.indexOf("\n", offset);
|
|
109
|
+
const end = nl === -1 ? text.length : nl > offset && text[nl - 1] === "\r" ? nl - 1 : nl;
|
|
110
|
+
if (offset === text.length && number > 0) break;
|
|
111
|
+
lines.push({
|
|
112
|
+
text: text.slice(offset, end),
|
|
113
|
+
offset,
|
|
114
|
+
number
|
|
115
|
+
});
|
|
116
|
+
if (nl === -1) break;
|
|
117
|
+
offset = nl + 1;
|
|
118
|
+
number++;
|
|
119
|
+
}
|
|
120
|
+
return lines;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Build the eager lint context: tokenize once, compose once (through the
|
|
124
|
+
* RECOVERED path, so the context exists for malformed input too — the
|
|
125
|
+
* `parse-validity` rule reports what went wrong).
|
|
126
|
+
*/
|
|
127
|
+
const buildContext = (text) => {
|
|
128
|
+
const tokens = Result.match(YamlTokens.tokenize(text), {
|
|
129
|
+
onSuccess: (value) => value,
|
|
130
|
+
onFailure: (error) => {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
text,
|
|
136
|
+
lines: buildLines(text),
|
|
137
|
+
tokens,
|
|
138
|
+
document: documentFromRaw(composeFirstDocument(text, { uniqueKeys: false }), text)
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
/** Resolve the effective severity of a configured entry. */
|
|
142
|
+
const resolveSeverity = (entry) => {
|
|
143
|
+
if (entry === "error" || entry === "warning") return entry;
|
|
144
|
+
if (typeof entry === "object" && (entry.severity === "error" || entry.severity === "warning")) return entry.severity;
|
|
145
|
+
return "error";
|
|
146
|
+
};
|
|
147
|
+
const byPosition = (a, b) => a.offset - b.offset || a.length - b.length || (a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : 0);
|
|
148
|
+
/**
|
|
149
|
+
* The rule loop shared by `run` and `fix` over one already-built context —
|
|
150
|
+
* `fix` must not tokenize and compose the same input twice.
|
|
151
|
+
*/
|
|
152
|
+
const runRules = (ctx, rules, config) => {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const rule of rules) {
|
|
155
|
+
const alwaysOn = rule.id === "parse-validity";
|
|
156
|
+
const entry = alwaysOn ? "error" : config.rules[rule.id];
|
|
157
|
+
if (entry === void 0 || entry === "off") continue;
|
|
158
|
+
const severity = resolveSeverity(entry);
|
|
159
|
+
const options = typeof entry === "object" ? entry : void 0;
|
|
160
|
+
for (const diagnostic of rule.check(ctx, options)) out.push(alwaysOn || diagnostic.severity === severity ? diagnostic : new YamlLintDiagnostic({
|
|
161
|
+
...diagnostic,
|
|
162
|
+
severity
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
return out.sort(byPosition);
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Linting statics. Not instantiable.
|
|
169
|
+
*
|
|
170
|
+
* @remarks
|
|
171
|
+
* Pure and synchronous throughout — the lint engine is the pure half only:
|
|
172
|
+
* strings in, diagnostics or a fixed string out. File discovery, config-file
|
|
173
|
+
* loading and autofix-to-disk belong to a consumer's tier, not here.
|
|
174
|
+
*
|
|
175
|
+
* @public
|
|
176
|
+
*/
|
|
177
|
+
var YamlLint = class {
|
|
178
|
+
constructor() {}
|
|
179
|
+
/**
|
|
180
|
+
* The built-in rule catalog. Custom usage is array concatenation:
|
|
181
|
+
* `YamlLint.run(text, [...YamlLint.builtins, myRule], config)`.
|
|
182
|
+
*/
|
|
183
|
+
static builtins = builtinRules;
|
|
184
|
+
/**
|
|
185
|
+
* Run `rules` over `text` under `config`, returning every finding sorted
|
|
186
|
+
* by position.
|
|
187
|
+
*
|
|
188
|
+
* Document-driven rules see the FIRST document of the stream (matching
|
|
189
|
+
* `Yaml.parse`; split the stream `Yaml.parseAll`-style to lint every
|
|
190
|
+
* document), while token- and line-driven rules cover the full source —
|
|
191
|
+
* see {@link LintContext}.
|
|
192
|
+
*
|
|
193
|
+
* A rule runs when its config entry is a severity or an options object;
|
|
194
|
+
* `"off"` and absent entries skip it — except `parse-validity`, which is
|
|
195
|
+
* always-on. The resolved severity (explicit-in-options, else the bare
|
|
196
|
+
* literal, else `"error"`) overrides what the rule emitted, again except
|
|
197
|
+
* for `parse-validity`, whose bridged engine diagnostics keep the
|
|
198
|
+
* engine's own grading.
|
|
199
|
+
*/
|
|
200
|
+
static run(text, rules, config) {
|
|
201
|
+
return runRules(buildContext(text), rules, config);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Run `rules` and apply every non-overlapping surgical fix, returning the
|
|
205
|
+
* fixed text. Fails with {@link YamlParseError} when the FIRST document
|
|
206
|
+
* of the stream has a fatal parse error — a document the engine cannot
|
|
207
|
+
* compose is not safely fixable. The gate matches `Yaml.parse`'s scope:
|
|
208
|
+
* later documents of a multi-document stream are not composed (split the
|
|
209
|
+
* stream `Yaml.parseAll`-style to lint every document), though token- and
|
|
210
|
+
* line-driven fixes still cover the full source. Fixes route exclusively through `YamlEdit.applyAll` (never a
|
|
211
|
+
* reformat), so applying them is comment-safe by construction; when two
|
|
212
|
+
* fixes overlap — or start at the same offset — the earlier one in
|
|
213
|
+
* {@link YamlLint.run} order (position, then rule id) wins and the later
|
|
214
|
+
* is dropped (its diagnostic remains reported by {@link YamlLint.run}).
|
|
215
|
+
*/
|
|
216
|
+
static fix(text, rules, config) {
|
|
217
|
+
const ctx = buildContext(text);
|
|
218
|
+
const fatal = ctx.document.errors.filter((e) => isFatalCode(e.code));
|
|
219
|
+
if (fatal.length > 0) return Result.fail(new YamlParseError({
|
|
220
|
+
diagnostics: fatal,
|
|
221
|
+
input: text
|
|
222
|
+
}));
|
|
223
|
+
const fixes = [];
|
|
224
|
+
let lastEnd = -1;
|
|
225
|
+
let lastOffset = -1;
|
|
226
|
+
for (const diagnostic of runRules(ctx, rules, config)) {
|
|
227
|
+
const fix = diagnostic.fix;
|
|
228
|
+
if (fix === void 0) continue;
|
|
229
|
+
if (fix.offset < lastEnd || fix.offset === lastOffset) continue;
|
|
230
|
+
fixes.push(fix);
|
|
231
|
+
lastEnd = fix.offset + fix.length;
|
|
232
|
+
lastOffset = fix.offset;
|
|
233
|
+
}
|
|
234
|
+
return Result.succeed(YamlEdit.applyAll(text, fixes));
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
//#endregion
|
|
239
|
+
export { YamlLint, YamlLintConfig, YamlLintRuleSetting };
|
package/YamlLintRule.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { YamlEdit } from "./YamlEdit.js";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/YamlLintRule.ts
|
|
5
|
+
/**
|
|
6
|
+
* Lint diagnostic severities. `"off"` is a config-level disable only and
|
|
7
|
+
* never reaches a diagnostic — a rule set to `"off"` is not run.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
const YamlLintSeverity = Schema.Literals(["error", "warning"]);
|
|
12
|
+
/**
|
|
13
|
+
* A single lint finding: the reporting rule, its severity, a positioned span
|
|
14
|
+
* and optionally a surgical fix.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately separate from the engine's `YamlDiagnostic`: that type is the
|
|
17
|
+
* lexer/parser/composer/stringifier error-code union and the single source
|
|
18
|
+
* of truth for engine fatality — it carries no severity and no fix, and
|
|
19
|
+
* lint-layer concerns must not pollute it. The `parse-validity` rule bridges
|
|
20
|
+
* the two by mapping engine diagnostics into this shape.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
var YamlLintDiagnostic = class extends Schema.Class("YamlLintDiagnostic")({
|
|
25
|
+
rule: Schema.String,
|
|
26
|
+
severity: YamlLintSeverity,
|
|
27
|
+
message: Schema.String,
|
|
28
|
+
offset: Schema.Number,
|
|
29
|
+
length: Schema.Number,
|
|
30
|
+
line: Schema.Number,
|
|
31
|
+
character: Schema.Number,
|
|
32
|
+
fix: Schema.optionalKey(YamlEdit)
|
|
33
|
+
}) {};
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { YamlLintDiagnostic, YamlLintSeverity };
|
package/YamlNode.js
CHANGED
|
@@ -49,9 +49,17 @@ const ScalarChomp = Schema.Literals([
|
|
|
49
49
|
* - `style` — the scalar presentation style in the source document.
|
|
50
50
|
* - `tag` — optional explicit YAML tag (e.g. `!!str`, `!!int`).
|
|
51
51
|
* - `anchor` — optional anchor name for aliasing.
|
|
52
|
-
* - `
|
|
52
|
+
* - `commentBefore` — own-line comment text directly above the node
|
|
53
|
+
* (multiple consecutive comment lines join with `\n`).
|
|
54
|
+
* - `comment` — trailing comment text on the node's line (strictly trailing;
|
|
55
|
+
* own-line comments live on `commentBefore`).
|
|
56
|
+
* - `spaceBefore` — `true` when a blank line precedes the node (and its
|
|
57
|
+
* `commentBefore` block, when present) in the source.
|
|
53
58
|
* - `chomp` — block-scalar chomping indicator, when the scalar is a block
|
|
54
59
|
* scalar.
|
|
60
|
+
* - `blockIndent` — the EXPLICIT indentation-indicator digit from a block
|
|
61
|
+
* scalar's header (`|2`, `>1+`), when the source spelled one; absent when
|
|
62
|
+
* the header let the reader auto-detect the indent.
|
|
55
63
|
* - `raw` — the raw source text, preserved when it differs from the resolved
|
|
56
64
|
* value in a way stringification needs to know about.
|
|
57
65
|
* - `sourceMultiline` — `true` when the source span covers two or more lines;
|
|
@@ -65,8 +73,11 @@ var YamlScalar = class extends Schema.TaggedClass()("YamlScalar", {
|
|
|
65
73
|
tag: Schema.optionalKey(Schema.String),
|
|
66
74
|
style: ScalarStyle,
|
|
67
75
|
anchor: Schema.optionalKey(Schema.String),
|
|
76
|
+
commentBefore: Schema.optionalKey(Schema.String),
|
|
68
77
|
comment: Schema.optionalKey(Schema.String),
|
|
78
|
+
spaceBefore: Schema.optionalKey(Schema.Boolean),
|
|
69
79
|
chomp: Schema.optionalKey(ScalarChomp),
|
|
80
|
+
blockIndent: Schema.optionalKey(Schema.Number),
|
|
70
81
|
raw: Schema.optionalKey(Schema.String),
|
|
71
82
|
sourceMultiline: Schema.optionalKey(Schema.Boolean),
|
|
72
83
|
offset: Schema.Number,
|
|
@@ -157,18 +168,31 @@ const YamlNode = Schema.suspend(() => Schema.Union([
|
|
|
157
168
|
* A YAML key-value pair AST node, representing one entry within a mapping.
|
|
158
169
|
* `value` is `null` when absent (e.g. `key:` with no value).
|
|
159
170
|
*
|
|
171
|
+
* - `commentBefore` — own-line comment text directly above the pair's key
|
|
172
|
+
* (multiple consecutive comment lines join with `\n`).
|
|
173
|
+
* - `comment` — trailing comment text on the pair's line (strictly trailing).
|
|
174
|
+
* - `spaceBefore` — `true` when a blank line precedes the pair (and its
|
|
175
|
+
* `commentBefore` block, when present) in the source.
|
|
176
|
+
*
|
|
160
177
|
* @public
|
|
161
178
|
*/
|
|
162
179
|
var YamlPair = class extends Schema.TaggedClass()("YamlPair", {
|
|
163
180
|
key: Schema.suspend(() => YamlNode),
|
|
164
181
|
value: Schema.NullOr(Schema.suspend(() => YamlNode)),
|
|
165
|
-
|
|
182
|
+
commentBefore: Schema.optionalKey(Schema.String),
|
|
183
|
+
comment: Schema.optionalKey(Schema.String),
|
|
184
|
+
spaceBefore: Schema.optionalKey(Schema.Boolean)
|
|
166
185
|
}) {};
|
|
167
186
|
/**
|
|
168
187
|
* A YAML mapping AST node, representing a collection of {@link YamlPair}
|
|
169
188
|
* entries.
|
|
170
189
|
*
|
|
171
190
|
* - `style` — the presentation style: `"block"` or `"flow"`.
|
|
191
|
+
* - `commentBefore` — own-line comment text directly above the mapping.
|
|
192
|
+
* - `comment` — trailing comment text: own-line comment lines after the
|
|
193
|
+
* mapping's last entry (still at the mapping's item indent), or a same-line
|
|
194
|
+
* trailing comment for a flow mapping.
|
|
195
|
+
* - `spaceBefore` — `true` when a blank line precedes the mapping.
|
|
172
196
|
* - `sourceMultiline` — `true` when the source span covers two or more lines;
|
|
173
197
|
* used by the canonical stringifier. Absent on synthetic nodes.
|
|
174
198
|
*
|
|
@@ -179,7 +203,9 @@ var YamlMap = class extends Schema.TaggedClass()("YamlMap", {
|
|
|
179
203
|
tag: Schema.optionalKey(Schema.String),
|
|
180
204
|
anchor: Schema.optionalKey(Schema.String),
|
|
181
205
|
style: CollectionStyle,
|
|
206
|
+
commentBefore: Schema.optionalKey(Schema.String),
|
|
182
207
|
comment: Schema.optionalKey(Schema.String),
|
|
208
|
+
spaceBefore: Schema.optionalKey(Schema.Boolean),
|
|
183
209
|
sourceMultiline: Schema.optionalKey(Schema.Boolean),
|
|
184
210
|
offset: Schema.Number,
|
|
185
211
|
length: Schema.Number
|
|
@@ -205,6 +231,12 @@ var YamlMap = class extends Schema.TaggedClass()("YamlMap", {
|
|
|
205
231
|
* A YAML sequence AST node, representing an ordered list of
|
|
206
232
|
* {@link (YamlNode:type)} values.
|
|
207
233
|
*
|
|
234
|
+
* - `commentBefore` — own-line comment text directly above the sequence.
|
|
235
|
+
* - `comment` — trailing comment text: own-line comment lines after the
|
|
236
|
+
* sequence's last item (still at the sequence's item indent), or a
|
|
237
|
+
* same-line trailing comment for a flow sequence.
|
|
238
|
+
* - `spaceBefore` — `true` when a blank line precedes the sequence.
|
|
239
|
+
*
|
|
208
240
|
* @public
|
|
209
241
|
*/
|
|
210
242
|
var YamlSeq = class extends Schema.TaggedClass()("YamlSeq", {
|
|
@@ -212,7 +244,9 @@ var YamlSeq = class extends Schema.TaggedClass()("YamlSeq", {
|
|
|
212
244
|
tag: Schema.optionalKey(Schema.String),
|
|
213
245
|
anchor: Schema.optionalKey(Schema.String),
|
|
214
246
|
style: CollectionStyle,
|
|
247
|
+
commentBefore: Schema.optionalKey(Schema.String),
|
|
215
248
|
comment: Schema.optionalKey(Schema.String),
|
|
249
|
+
spaceBefore: Schema.optionalKey(Schema.Boolean),
|
|
216
250
|
sourceMultiline: Schema.optionalKey(Schema.Boolean),
|
|
217
251
|
offset: Schema.Number,
|
|
218
252
|
length: Schema.Number
|