@effected/yaml 0.10.0 → 0.11.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 +23 -6
- package/YamlFormat.js +74 -7
- package/YamlLint.js +0 -0
- package/YamlLintRule.js +50 -1
- package/index.d.ts +246 -3
- package/index.js +3 -3
- package/internal/requote.js +48 -0
- package/internal/rules/comments-spacing.js +48 -9
- package/internal/rules/document-end.js +48 -18
- package/internal/rules/document-start.js +31 -5
- package/internal/rules/empty-lines.js +21 -1
- package/internal/rules/indentation.js +76 -23
- package/internal/rules/line-length.js +9 -1
- package/internal/rules/quoted-strings.js +29 -11
- package/internal/stringifier.js +1 -1
- package/package.json +1 -1
- package/tsdoc-metadata.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
|
|
8
|
-
Zero-dependency YAML 1.2 parsing, editing, formatting and linting expressed as Effect schemas and pure functions. Parse a single document or a multi-document stream into plain values or an offset-preserving AST, resolve anchors and aliases, read and preserve per-node comments, strip comments, compute byte-minimal edits, format, modify by path, walk a document as a `Stream` of AST events or positioned tokens, lint it against a rule catalog with surgical autofix
|
|
8
|
+
Zero-dependency YAML 1.2 parsing, editing, formatting and linting expressed as Effect schemas and pure functions. Parse a single document or a multi-document stream into plain values or an offset-preserving AST, resolve anchors and aliases, read and preserve per-node comments, strip comments, compute byte-minimal edits, format, modify by path, walk a document as a `Stream` of AST events or positioned tokens, lint it against a rule catalog with surgical autofix — inferring the lint config from files you already have — and decode straight into a validated domain schema.
|
|
9
9
|
|
|
10
10
|
> **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
|
|
11
11
|
> development against a single pinned Effect v4 prerelease. Packages graduate to
|
|
@@ -156,6 +156,23 @@ console.log(Result.isSuccess(fixed) ? fixed.success : "fatal parse error — not
|
|
|
156
156
|
|
|
157
157
|
Positions are zero-based throughout, so the first finding above sits on the second line of the source. Fixes route through `YamlEdit.applyAll` rather than a reformat, so an autofix cannot move a comment it was not asked to touch, and a rule whose repair would need reflowing the document ships without a fix at all. `YamlLintConfig.default` and `YamlLintConfig.relaxed` are the presets; compose your own by spreading a preset's `rules` into `YamlLintConfig.make`. Config validation is rule-aware for the built-in catalog — a misspelled option key on a built-in rule or an attempt to switch off the always-on `parse-validity` rule fails schema validation instead of being quietly ignored — while an unknown (custom) rule id passes through with its options treated as opaque, left for the custom rule itself to validate. Custom rules are plain array concatenation — `YamlLint.run(text, [...YamlLint.builtins, myRule], config)` — and configuring `myRule` in that config works precisely because of the pass-through.
|
|
158
158
|
|
|
159
|
+
A repo with no lint config yet can infer one from what its files already do. `YamlLint.inferLenient` observes a text's style — quote type, indentation width, document markers and the rest — and returns the config the dominant style implies, plus the residual: the diagnostics that config still produces on the same text.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { YamlLint } from "@effected/yaml";
|
|
163
|
+
|
|
164
|
+
const workflow = ["---", "name: ci", "on:", " push:", " branches: ['main']", "jobs:", " build:", " runs-on: ubuntu-latest", ""].join("\n");
|
|
165
|
+
|
|
166
|
+
const { config, residual } = YamlLint.inferLenient(workflow, YamlLint.builtins);
|
|
167
|
+
console.log(config.rules["quoted-strings"], config.rules["document-start"], config.rules.indentation);
|
|
168
|
+
// { quoteType: 'single' } { present: true } { spaces: 2 }
|
|
169
|
+
|
|
170
|
+
for (const d of residual) console.log(`${d.line}:${d.character} ${d.rule} ${d.message}`);
|
|
171
|
+
// 2:0 truthy Truthy value "on" is not in the allowed spellings
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`YamlLint.inferStrict` is the same step in `Result` form: it fails with a structured `YamlStyleConflictError` — naming every conflicting dimension, all observed spellings, counts and positions — when the observed style disagrees with itself, instead of picking a winner. Across many files, compose the primitives instead of the conveniences: `YamlLint.observe` each file, merge the `StyleEvidence` values with `StyleEvidence.combine` (evidence is a monoid, so the order does not matter), then resolve once with `YamlLint.resolveStrict` or `YamlLint.resolveLenient`. Dimensions never observed fall back to the base config rather than failing. Policy rules with no detectable style — `truthy`, `key-duplicates` — stay default-driven, as do `line-length` and `empty-lines`, whose observations are floor-only: the longest line proves the limit is at least that long, not what it should be, so the evidence is carried but never resolved into config. A custom rule participates by implementing the optional `infer` hook on `YamlRule`.
|
|
175
|
+
|
|
159
176
|
## Migrating from Prettier
|
|
160
177
|
|
|
161
178
|
`YamlFormat.formatToString(text, undefined, options)` replaces a `prettier --write` over YAML files. The option mapping:
|
|
@@ -163,14 +180,14 @@ Positions are zero-based throughout, so the first finding above sits on the seco
|
|
|
163
180
|
| Prettier | `YamlFormattingOptions` |
|
|
164
181
|
| --- | --- |
|
|
165
182
|
| `tabWidth` | `indent` |
|
|
166
|
-
| `singleQuote` | `quoteStyle: "single" \| "double"` |
|
|
183
|
+
| `singleQuote` | `quoteStyle: "single" \| "double"` + `requoteScalars: true` |
|
|
167
184
|
| indented block sequences (always on) | `indentSequences: true` |
|
|
168
185
|
| `printWidth` / `proseWrap` | no analog — see below |
|
|
169
186
|
| comments (best-effort) | `preserveComments: true` (the default) |
|
|
170
187
|
|
|
171
188
|
Two defaults differ from Prettier's output and are worth setting explicitly. The stringify default is `quoteStyle: "single"` for byte-compatibility with the kit's legacy form, so if you ran Prettier with its own default (`singleQuote: false`), set `quoteStyle: "double"`. Likewise `indentSequences` defaults to `false` — the flat legacy form with the `-` markers at the key's column — while Prettier always indented block sequences one level, so an ex-Prettier consumer almost certainly wants `indentSequences: true`.
|
|
172
189
|
|
|
173
|
-
|
|
190
|
+
The `requoteScalars: true` half of the `singleQuote` row is what buys full parity. By default, `quoteStyle` on the format path governs only quotes the stringifier *introduces* — a scalar already quoted in your source keeps its own quote style, so formatting `a: 'x'` with `quoteStyle: "double"` leaves `'x'` untouched, where Prettier would have rewritten it to `"x"`. With `requoteScalars: true` (default `false`), `quoteStyle` applies to already-quoted source scalars too. A re-quote happens only when it provably preserves the parsed value: single→double applies proper double-quote escaping, and double→single is skipped when the value carries characters single quotes cannot express. Plain scalars stay plain, block scalars stay block, and tagged, anchored and multi-line scalars are never touched. One interaction to watch: `quoteStyle` omitted defaults to `"single"`, so `requoteScalars: true` on its own converts `"x"` to `'x'` — an ex-Prettier consumer who ran with `singleQuote: false` wants `quoteStyle: "double"` alongside it.
|
|
174
191
|
|
|
175
192
|
`printWidth` and `proseWrap` have no analog here: `lineWidth` folding is a value-path feature only (`Yaml.stringify` / `Yaml.stringifyResult`), and the option is deliberately inert on the format path by contract — `YamlFormat` never reflows scalar content (the `lineWidth` TSDoc on `YamlStringifyOptions` pins this). Comments, which Prettier handled best-effort, are preserved exactly by default.
|
|
176
193
|
|
|
@@ -186,11 +203,11 @@ Multi-document streams — a Kubernetes manifest, or the two-document `pnpm-lock
|
|
|
186
203
|
- `Yaml.schema` / `Yaml.fromString` / `Yaml.YamlFromString` / `Yaml.allFromString` — string→domain schema factories that decode a single document or a whole stream into a validated Effect `Schema` value.
|
|
187
204
|
- `YamlNode` — an offset-preserving AST (`YamlScalar`, `YamlMap`, `YamlSeq`, `YamlPair`, `YamlAlias`) for locating and reading nodes by position or path; every node class except `YamlPair` carries its own `commentBefore`, `comment` and `spaceBefore`, and a pair is just its `key` and `value`.
|
|
188
205
|
- `YamlDocument` — the full parsed AST plus the recovered `errors` and `warnings` arrays, so a partially valid document is still inspectable, and the header and trailing comment blocks as `commentBefore` / `comment`, the same two names the node classes use.
|
|
189
|
-
- `YamlEdit` / `YamlFormat` — compute byte-minimal edit arrays for formatting and path-based modification, so a change preserves every comment and byte you did not touch; `preserveComments: false` opts out. `format` handles multi-document streams whole, re-emitting every document and its framing; `modify` is single-document by contract (a path names no particular document of a stream) and fails typed with `MultiDocumentStream` rather than guessing.
|
|
206
|
+
- `YamlEdit` / `YamlFormat` — compute byte-minimal edit arrays for formatting and path-based modification, so a change preserves every comment and byte you did not touch; `preserveComments: false` opts out, and opt-in `requoteScalars` applies `quoteStyle` to scalars already quoted in the source, semantics-preserving or skipped. `format` handles multi-document streams whole, re-emitting every document and its framing; `modify` is single-document by contract (a path names no particular document of a stream) and fails typed with `MultiDocumentStream` rather than guessing.
|
|
190
207
|
- `YamlVisitor` — walk a parsed document as a `Stream` of a tagged-enum event union, with `Stream.take` early termination on large inputs and a `placement` on every comment event.
|
|
191
208
|
- `YamlToken` / `YamlTokens` — the positioned token stream the lint rules read: `tokenize` is a synchronous `Result` that always succeeds, surfacing lexical problems as `error`-kind tokens, and `stream` derives an Effect `Stream` from the same walk.
|
|
192
|
-
- `YamlLint` — `run`, `fix` and the `builtins` catalog: 14 rules covering parse validity, line length, trailing whitespace, blank lines, end-of-file newline, document markers, duplicate keys, quoting, truthy spellings, comment and colon and hyphen spacing, and indentation.
|
|
193
|
-
- `YamlLintConfig` / `YamlLintRule` / `YamlLintDiagnostic` — the `default` and `relaxed` presets, a config schema validated against the rule catalog, and the model a custom rule implements.
|
|
209
|
+
- `YamlLint` — `run`, `fix` and the `builtins` catalog: 14 rules covering parse validity, line length, trailing whitespace, blank lines, end-of-file newline, document markers, duplicate keys, quoting, truthy spellings, comment and colon and hyphen spacing, and indentation. Plus config inference for adopting the linter on an existing corpus: `observe` produces mergeable `StyleEvidence`, `resolveStrict` fails typed with `YamlStyleConflictError` on conflicting spellings, `resolveLenient` picks the dominant style, and `inferStrict` / `inferLenient` do it in one step per text.
|
|
210
|
+
- `YamlLintConfig` / `YamlLintRule` / `YamlLintDiagnostic` — the `default` and `relaxed` presets, a config schema validated against the rule catalog, and the model a custom rule implements, including the optional `infer` hook that lets a custom rule participate in config inference.
|
|
194
211
|
- `YamlParseError` / `YamlStringifyError` / `YamlModificationError` — tagged errors carrying structured, positional `YamlDiagnostic` arrays rather than opaque messages.
|
|
195
212
|
|
|
196
213
|
## License
|
package/YamlFormat.js
CHANGED
|
@@ -6,6 +6,7 @@ import { YamlDiagnostic } from "./YamlDiagnostic.js";
|
|
|
6
6
|
import { YamlStringifyOptions } from "./Yaml.js";
|
|
7
7
|
import { YamlEdit, YamlRange } from "./YamlEdit.js";
|
|
8
8
|
import { computeEdits } from "./internal/diff.js";
|
|
9
|
+
import { requoteScalarText } from "./internal/requote.js";
|
|
9
10
|
import { Effect, Schema } from "effect";
|
|
10
11
|
|
|
11
12
|
//#region src/YamlFormat.ts
|
|
@@ -13,9 +14,25 @@ import { Effect, Schema } from "effect";
|
|
|
13
14
|
* Options controlling formatting behavior: every {@link YamlStringifyOptions}
|
|
14
15
|
* field (derived, not hand-duplicated — including `indentSequences` and
|
|
15
16
|
* `quoteStyle`) plus
|
|
16
|
-
* `preserveComments` (default `true`)
|
|
17
|
+
* `preserveComments` (default `true`), `range` (restrict edits to a
|
|
17
18
|
* region; see the module-level remarks on the `range` parameter vs. this
|
|
18
|
-
* field).
|
|
19
|
+
* field) and `requoteScalars` (default `false`).
|
|
20
|
+
*
|
|
21
|
+
* `requoteScalars` makes `quoteStyle` apply to scalars **already quoted in
|
|
22
|
+
* the source** on the format path — by default formatting preserves an
|
|
23
|
+
* existing scalar's own quote style, and `quoteStyle` governs only quotes the
|
|
24
|
+
* stringifier introduces. When enabled, a re-quote happens only when it
|
|
25
|
+
* provably preserves the parsed value: single→double applies proper
|
|
26
|
+
* double-quote escaping, double→single is skipped whenever the value carries
|
|
27
|
+
* characters single quotes cannot express (newlines, tabs, control and other
|
|
28
|
+
* non-printable characters — single-quoted style can escape nothing but
|
|
29
|
+
* `'`). Plain scalars stay plain, block scalars stay block, and scalars
|
|
30
|
+
* carrying a tag or anchor — or spanning multiple source lines — are left
|
|
31
|
+
* untouched. The option exists only where a source quote exists to re-quote:
|
|
32
|
+
* it is read by {@link YamlFormat.format} / {@link YamlFormat.formatToString}
|
|
33
|
+
* and is deliberately absent from `Yaml.stringify` (which serializes plain
|
|
34
|
+
* values) and {@link YamlFormat.modify} (which takes a bare
|
|
35
|
+
* {@link YamlStringifyOptions}).
|
|
19
36
|
*
|
|
20
37
|
* Construct with the validated `YamlFormattingOptions.make({ ... })` static —
|
|
21
38
|
* the kit convention (never `new`). Call sites that take a
|
|
@@ -37,7 +54,8 @@ import { Effect, Schema } from "effect";
|
|
|
37
54
|
var YamlFormattingOptions = class extends Schema.Class("YamlFormattingOptions")({
|
|
38
55
|
...YamlStringifyOptions.fields,
|
|
39
56
|
preserveComments: Schema.optionalKey(Schema.Boolean),
|
|
40
|
-
range: Schema.optionalKey(YamlRange)
|
|
57
|
+
range: Schema.optionalKey(YamlRange),
|
|
58
|
+
requoteScalars: Schema.optionalKey(Schema.Boolean)
|
|
41
59
|
}) {};
|
|
42
60
|
/**
|
|
43
61
|
* Raised when `YamlFormat.modify` cannot navigate the requested path against
|
|
@@ -104,6 +122,49 @@ function definedFields(fields) {
|
|
|
104
122
|
for (const key of Object.keys(fields)) if (fields[key] !== void 0) out[key] = fields[key];
|
|
105
123
|
return out;
|
|
106
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Walk a composed AST and flip the `style` of every scalar the shared
|
|
127
|
+
* escaping-mode helper deems re-quotable to the target quote's style. The
|
|
128
|
+
* stringifier then emits those scalars through the very renderers the helper
|
|
129
|
+
* used to prove the replacement value-preserving, so flip-and-stringify and
|
|
130
|
+
* the helper's replacement text cannot disagree. Everything the helper skips
|
|
131
|
+
* — plain scalars, block scalars, tagged/anchored nodes, multi-line source
|
|
132
|
+
* spans, impossible double→single escapes — keeps its node untouched, and
|
|
133
|
+
* the surrounding diff keeps the edit surgical per scalar span.
|
|
134
|
+
*/
|
|
135
|
+
function requoteNode(node, text, quote) {
|
|
136
|
+
if (node instanceof YamlScalar) {
|
|
137
|
+
if (requoteScalarText(text, node, quote, "escaping") === void 0) return node;
|
|
138
|
+
return YamlScalar.make({
|
|
139
|
+
value: node.value,
|
|
140
|
+
style: quote === "\"" ? "double-quoted" : "single-quoted",
|
|
141
|
+
...definedFields({
|
|
142
|
+
commentBefore: node.commentBefore,
|
|
143
|
+
comment: node.comment,
|
|
144
|
+
spaceBefore: node.spaceBefore,
|
|
145
|
+
raw: node.raw,
|
|
146
|
+
sourceMultiline: node.sourceMultiline
|
|
147
|
+
}),
|
|
148
|
+
offset: node.offset,
|
|
149
|
+
length: node.length
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (node instanceof YamlMap) return rebuildMap(node, node.items.map((pair) => YamlPair.make({
|
|
153
|
+
key: requoteNode(pair.key, text, quote),
|
|
154
|
+
value: pair.value === null ? null : requoteNode(pair.value, text, quote)
|
|
155
|
+
})));
|
|
156
|
+
if (node instanceof YamlSeq) return rebuildSeq(node, node.items.map((item) => requoteNode(item, text, quote)));
|
|
157
|
+
return node;
|
|
158
|
+
}
|
|
159
|
+
/** Apply the opt-in re-quoting pass to a composed document's contents. */
|
|
160
|
+
function requoteDocument(doc, text, options) {
|
|
161
|
+
if (options?.requoteScalars !== true || doc.contents === null) return doc;
|
|
162
|
+
const quote = (options.quoteStyle ?? "single") === "double" ? "\"" : "'";
|
|
163
|
+
return {
|
|
164
|
+
...doc,
|
|
165
|
+
contents: requoteNode(doc.contents, text, quote)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
107
168
|
/** Rebuild a composed document for re-emission, stripping comments when asked. */
|
|
108
169
|
function toOutputDocument(doc, preserveComments) {
|
|
109
170
|
return {
|
|
@@ -132,12 +193,12 @@ function toOutputDocument(doc, preserveComments) {
|
|
|
132
193
|
*/
|
|
133
194
|
function formatDocument(text, options) {
|
|
134
195
|
const { documents, streamErrors } = composeAllDocuments(text, {});
|
|
135
|
-
if (documents.length > 1) return formatStream(documents, streamErrors, options);
|
|
196
|
+
if (documents.length > 1) return formatStream(text, documents, streamErrors, options);
|
|
136
197
|
if (streamErrors.some((e) => isFatalCode(e.code))) return void 0;
|
|
137
198
|
const doc = documents[0] ?? EMPTY_DOCUMENT;
|
|
138
199
|
if (doc.errors.some((e) => isFatalCode(e.code))) return void 0;
|
|
139
200
|
if (doc.directives.length > 0) return void 0;
|
|
140
|
-
return stringifyDocument(toOutputDocument(doc, options?.preserveComments ?? true), toStringifyInput(options));
|
|
201
|
+
return stringifyDocument(toOutputDocument(requoteDocument(doc, text, options), options?.preserveComments ?? true), toStringifyInput(options));
|
|
141
202
|
}
|
|
142
203
|
/**
|
|
143
204
|
* Format a multi-document stream: every document is re-emitted in order with
|
|
@@ -158,7 +219,7 @@ function formatDocument(text, options) {
|
|
|
158
219
|
* line while keeping the dependent shorthand tags produced unparseable
|
|
159
220
|
* output (probe-verified downstream against an independent oracle).
|
|
160
221
|
*/
|
|
161
|
-
function formatStream(documents, streamErrors, options) {
|
|
222
|
+
function formatStream(text, documents, streamErrors, options) {
|
|
162
223
|
if (streamErrors.some((e) => isFatalCode(e.code))) return void 0;
|
|
163
224
|
if (documents.some((d) => d.errors.some((e) => isFatalCode(e.code)))) return void 0;
|
|
164
225
|
if (documents.some((d) => d.directives.length > 0)) return void 0;
|
|
@@ -172,7 +233,7 @@ function formatStream(documents, streamErrors, options) {
|
|
|
172
233
|
...base,
|
|
173
234
|
finalNewline: true
|
|
174
235
|
} : base;
|
|
175
|
-
parts.push(stringifyDocument(toOutputDocument(doc, preserveComments), docOptions));
|
|
236
|
+
parts.push(stringifyDocument(toOutputDocument(requoteDocument(doc, text, options), preserveComments), docOptions));
|
|
176
237
|
}
|
|
177
238
|
return parts.join("");
|
|
178
239
|
}
|
|
@@ -351,6 +412,12 @@ var YamlFormat = class YamlFormat {
|
|
|
351
412
|
* `{ offset, length }` object as well as a {@link YamlRange} instance, so
|
|
352
413
|
* callers do not need `YamlRange.make(...)` for the common case.
|
|
353
414
|
*
|
|
415
|
+
* Formatting preserves an existing scalar's own quote style by default —
|
|
416
|
+
* `quoteStyle` governs only quotes the stringifier introduces. The opt-in
|
|
417
|
+
* `options.requoteScalars` makes `quoteStyle` apply to already-quoted
|
|
418
|
+
* source scalars too, re-quoting only where the parsed value is provably
|
|
419
|
+
* preserved; see {@link YamlFormattingOptions} for the exact skip rules.
|
|
420
|
+
*
|
|
354
421
|
* A plain `<<` mapping key is preserved unquoted, keeping its merge-key
|
|
355
422
|
* meaning (`tag:yaml.org,2002:merge`) — quoting it to `'<<'` would produce
|
|
356
423
|
* an ordinary string key that merges nothing, changing what the document
|
package/YamlLint.js
CHANGED
|
Binary file
|
package/YamlLintRule.js
CHANGED
|
@@ -31,6 +31,55 @@ var YamlLintDiagnostic = class extends Schema.Class("YamlLintDiagnostic")({
|
|
|
31
31
|
character: Schema.Number,
|
|
32
32
|
fix: Schema.optionalKey(YamlEdit)
|
|
33
33
|
}) {};
|
|
34
|
+
/**
|
|
35
|
+
* One categorical style observation (#345): a single occurrence of a style
|
|
36
|
+
* choice in the source, voting a `value` for an inference `dimension`.
|
|
37
|
+
*
|
|
38
|
+
* The `dimension` IS the rule's option key and the `value` IS that option's
|
|
39
|
+
* value (`"double"` for `quoteType`, `2` for `spaces`, `false` for
|
|
40
|
+
* `present`), so the inference resolvers can turn unanimous or dominant
|
|
41
|
+
* votes into a config entry with no per-rule knowledge — which is what lets
|
|
42
|
+
* custom rules participate in inference for free. The position points at the
|
|
43
|
+
* occurrence that voted, so a strict-resolution conflict can name where each
|
|
44
|
+
* spelling was seen.
|
|
45
|
+
*
|
|
46
|
+
* The `_tag` literal is the RUNTIME discriminator between the two
|
|
47
|
+
* observation kinds: class instances are structurally assignable, so a
|
|
48
|
+
* custom rule may yield a plain object shaped like a vote, and the evidence
|
|
49
|
+
* builder must still sort it into the right tally without `instanceof`.
|
|
50
|
+
* `.make` defaults it — construction sites never pass `_tag`.
|
|
51
|
+
*
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
var StyleVote = class extends Schema.TaggedClass()("StyleVote", {
|
|
55
|
+
dimension: Schema.String,
|
|
56
|
+
value: Schema.Union([
|
|
57
|
+
Schema.String,
|
|
58
|
+
Schema.Number,
|
|
59
|
+
Schema.Boolean
|
|
60
|
+
]),
|
|
61
|
+
offset: Schema.Number,
|
|
62
|
+
length: Schema.Number,
|
|
63
|
+
line: Schema.Number,
|
|
64
|
+
character: Schema.Number
|
|
65
|
+
}) {};
|
|
66
|
+
/**
|
|
67
|
+
* One measured style floor (#345): a value the source PROVES is at least
|
|
68
|
+
* `value`, without proving what the configured limit should be — the longest
|
|
69
|
+
* observed line proves `line-length.max` is at least that long, not what it
|
|
70
|
+
* is. Floors are carried in the evidence for callers that want them and are
|
|
71
|
+
* never resolved into config options; fabricating a max from the largest
|
|
72
|
+
* value one happened to see would be lying with a straight face.
|
|
73
|
+
*
|
|
74
|
+
* The `_tag` literal discriminates a floor from a {@link StyleVote} at
|
|
75
|
+
* runtime (see there); `.make` defaults it.
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
var StyleFloor = class extends Schema.TaggedClass()("StyleFloor", {
|
|
80
|
+
dimension: Schema.String,
|
|
81
|
+
value: Schema.Number
|
|
82
|
+
}) {};
|
|
34
83
|
|
|
35
84
|
//#endregion
|
|
36
|
-
export { YamlLintDiagnostic, YamlLintSeverity };
|
|
85
|
+
export { StyleFloor, StyleVote, YamlLintDiagnostic, YamlLintSeverity };
|
package/index.d.ts
CHANGED
|
@@ -1009,14 +1009,31 @@ declare const YamlFormattingOptions_base: Schema.Class<YamlFormattingOptions, Sc
|
|
|
1009
1009
|
readonly forceDefaultStyles: Schema.optionalKey<Schema.Boolean>;
|
|
1010
1010
|
readonly preserveComments: Schema.optionalKey<Schema.Boolean>;
|
|
1011
1011
|
readonly range: Schema.optionalKey<typeof YamlRange>;
|
|
1012
|
+
readonly requoteScalars: Schema.optionalKey<Schema.Boolean>;
|
|
1012
1013
|
}>, {}>;
|
|
1013
1014
|
/**
|
|
1014
1015
|
* Options controlling formatting behavior: every {@link YamlStringifyOptions}
|
|
1015
1016
|
* field (derived, not hand-duplicated — including `indentSequences` and
|
|
1016
1017
|
* `quoteStyle`) plus
|
|
1017
|
-
* `preserveComments` (default `true`)
|
|
1018
|
+
* `preserveComments` (default `true`), `range` (restrict edits to a
|
|
1018
1019
|
* region; see the module-level remarks on the `range` parameter vs. this
|
|
1019
|
-
* field).
|
|
1020
|
+
* field) and `requoteScalars` (default `false`).
|
|
1021
|
+
*
|
|
1022
|
+
* `requoteScalars` makes `quoteStyle` apply to scalars **already quoted in
|
|
1023
|
+
* the source** on the format path — by default formatting preserves an
|
|
1024
|
+
* existing scalar's own quote style, and `quoteStyle` governs only quotes the
|
|
1025
|
+
* stringifier introduces. When enabled, a re-quote happens only when it
|
|
1026
|
+
* provably preserves the parsed value: single→double applies proper
|
|
1027
|
+
* double-quote escaping, double→single is skipped whenever the value carries
|
|
1028
|
+
* characters single quotes cannot express (newlines, tabs, control and other
|
|
1029
|
+
* non-printable characters — single-quoted style can escape nothing but
|
|
1030
|
+
* `'`). Plain scalars stay plain, block scalars stay block, and scalars
|
|
1031
|
+
* carrying a tag or anchor — or spanning multiple source lines — are left
|
|
1032
|
+
* untouched. The option exists only where a source quote exists to re-quote:
|
|
1033
|
+
* it is read by {@link YamlFormat.format} / {@link YamlFormat.formatToString}
|
|
1034
|
+
* and is deliberately absent from `Yaml.stringify` (which serializes plain
|
|
1035
|
+
* values) and {@link YamlFormat.modify} (which takes a bare
|
|
1036
|
+
* {@link YamlStringifyOptions}).
|
|
1020
1037
|
*
|
|
1021
1038
|
* Construct with the validated `YamlFormattingOptions.make({ ... })` static —
|
|
1022
1039
|
* the kit convention (never `new`). Call sites that take a
|
|
@@ -1122,6 +1139,12 @@ declare class YamlFormat {
|
|
|
1122
1139
|
* `{ offset, length }` object as well as a {@link YamlRange} instance, so
|
|
1123
1140
|
* callers do not need `YamlRange.make(...)` for the common case.
|
|
1124
1141
|
*
|
|
1142
|
+
* Formatting preserves an existing scalar's own quote style by default —
|
|
1143
|
+
* `quoteStyle` governs only quotes the stringifier introduces. The opt-in
|
|
1144
|
+
* `options.requoteScalars` makes `quoteStyle` apply to already-quoted
|
|
1145
|
+
* source scalars too, re-quoting only where the parsed value is provably
|
|
1146
|
+
* preserved; see {@link YamlFormattingOptions} for the exact skip rules.
|
|
1147
|
+
*
|
|
1125
1148
|
* A plain `<<` mapping key is preserved unquoted, keeping its merge-key
|
|
1126
1149
|
* meaning (`tag:yaml.org,2002:merge`) — quoting it to `'<<'` would produce
|
|
1127
1150
|
* an ordinary string key that merges nothing, changing what the document
|
|
@@ -1337,6 +1360,60 @@ interface LintContext {
|
|
|
1337
1360
|
readonly tokens: ReadonlyArray<YamlToken>;
|
|
1338
1361
|
readonly document: YamlDocument;
|
|
1339
1362
|
}
|
|
1363
|
+
declare const StyleVote_base: Schema.Class<StyleVote, Schema.TaggedStruct<"StyleVote", {
|
|
1364
|
+
readonly dimension: Schema.String;
|
|
1365
|
+
readonly value: Schema.Union<readonly [Schema.String, Schema.Number, Schema.Boolean]>;
|
|
1366
|
+
readonly offset: Schema.Number;
|
|
1367
|
+
readonly length: Schema.Number;
|
|
1368
|
+
readonly line: Schema.Number;
|
|
1369
|
+
readonly character: Schema.Number;
|
|
1370
|
+
}>, {}>;
|
|
1371
|
+
/**
|
|
1372
|
+
* One categorical style observation (#345): a single occurrence of a style
|
|
1373
|
+
* choice in the source, voting a `value` for an inference `dimension`.
|
|
1374
|
+
*
|
|
1375
|
+
* The `dimension` IS the rule's option key and the `value` IS that option's
|
|
1376
|
+
* value (`"double"` for `quoteType`, `2` for `spaces`, `false` for
|
|
1377
|
+
* `present`), so the inference resolvers can turn unanimous or dominant
|
|
1378
|
+
* votes into a config entry with no per-rule knowledge — which is what lets
|
|
1379
|
+
* custom rules participate in inference for free. The position points at the
|
|
1380
|
+
* occurrence that voted, so a strict-resolution conflict can name where each
|
|
1381
|
+
* spelling was seen.
|
|
1382
|
+
*
|
|
1383
|
+
* The `_tag` literal is the RUNTIME discriminator between the two
|
|
1384
|
+
* observation kinds: class instances are structurally assignable, so a
|
|
1385
|
+
* custom rule may yield a plain object shaped like a vote, and the evidence
|
|
1386
|
+
* builder must still sort it into the right tally without `instanceof`.
|
|
1387
|
+
* `.make` defaults it — construction sites never pass `_tag`.
|
|
1388
|
+
*
|
|
1389
|
+
* @public
|
|
1390
|
+
*/
|
|
1391
|
+
declare class StyleVote extends StyleVote_base {}
|
|
1392
|
+
declare const StyleFloor_base: Schema.Class<StyleFloor, Schema.TaggedStruct<"StyleFloor", {
|
|
1393
|
+
readonly dimension: Schema.String;
|
|
1394
|
+
readonly value: Schema.Number;
|
|
1395
|
+
}>, {}>;
|
|
1396
|
+
/**
|
|
1397
|
+
* One measured style floor (#345): a value the source PROVES is at least
|
|
1398
|
+
* `value`, without proving what the configured limit should be — the longest
|
|
1399
|
+
* observed line proves `line-length.max` is at least that long, not what it
|
|
1400
|
+
* is. Floors are carried in the evidence for callers that want them and are
|
|
1401
|
+
* never resolved into config options; fabricating a max from the largest
|
|
1402
|
+
* value one happened to see would be lying with a straight face.
|
|
1403
|
+
*
|
|
1404
|
+
* The `_tag` literal discriminates a floor from a {@link StyleVote} at
|
|
1405
|
+
* runtime (see there); `.make` defaults it.
|
|
1406
|
+
*
|
|
1407
|
+
* @public
|
|
1408
|
+
*/
|
|
1409
|
+
declare class StyleFloor extends StyleFloor_base {}
|
|
1410
|
+
/**
|
|
1411
|
+
* What a rule's `infer` hook yields per occurrence: a categorical
|
|
1412
|
+
* {@link StyleVote} or a measured {@link StyleFloor}.
|
|
1413
|
+
*
|
|
1414
|
+
* @public
|
|
1415
|
+
*/
|
|
1416
|
+
type StyleObservation = StyleVote | StyleFloor;
|
|
1340
1417
|
/**
|
|
1341
1418
|
* The public rule interface — built-ins and custom rules are the same
|
|
1342
1419
|
* shape, and config references either by `id`; there is no privileged
|
|
@@ -1347,11 +1424,19 @@ interface LintContext {
|
|
|
1347
1424
|
* rules receive options already validated against their exported options
|
|
1348
1425
|
* schema, custom rules validate their own.
|
|
1349
1426
|
*
|
|
1427
|
+
* `infer` is the optional config-inference hook (#345): it reports the style
|
|
1428
|
+
* the source already follows as per-occurrence {@link StyleObservation}s, so
|
|
1429
|
+
* detection logic lives beside the check logic that polices the same
|
|
1430
|
+
* dimension (and shares its fixtures). Rules with no detectable style — the
|
|
1431
|
+
* policy rules, whose only evidence is violations — simply omit the hook and
|
|
1432
|
+
* stay default-driven under every resolver.
|
|
1433
|
+
*
|
|
1350
1434
|
* @public
|
|
1351
1435
|
*/
|
|
1352
1436
|
interface YamlRule {
|
|
1353
1437
|
readonly id: string;
|
|
1354
1438
|
readonly check: (ctx: LintContext, options: unknown) => Iterable<YamlLintDiagnostic>;
|
|
1439
|
+
readonly infer?: (ctx: LintContext) => Iterable<StyleObservation>;
|
|
1355
1440
|
}
|
|
1356
1441
|
//#endregion
|
|
1357
1442
|
//#region src/YamlLint.d.ts
|
|
@@ -1399,6 +1484,118 @@ declare class YamlLintConfig extends YamlLintConfig_base {
|
|
|
1399
1484
|
/** The relaxed preset: style rules demoted to warnings. */
|
|
1400
1485
|
static readonly relaxed: YamlLintConfig;
|
|
1401
1486
|
}
|
|
1487
|
+
declare const StyleVoteTally_base: Schema.Class<StyleVoteTally, Schema.Struct<{
|
|
1488
|
+
readonly rule: Schema.String;
|
|
1489
|
+
readonly dimension: Schema.String;
|
|
1490
|
+
readonly value: Schema.Union<readonly [Schema.String, Schema.Number, Schema.Boolean]>;
|
|
1491
|
+
readonly count: Schema.Number;
|
|
1492
|
+
readonly offset: Schema.Number;
|
|
1493
|
+
readonly length: Schema.Number;
|
|
1494
|
+
readonly line: Schema.Number;
|
|
1495
|
+
readonly character: Schema.Number;
|
|
1496
|
+
}>, {}>;
|
|
1497
|
+
/**
|
|
1498
|
+
* An accumulated tally of one {@link StyleVote} spelling: how many times a
|
|
1499
|
+
* `value` was voted for a `(rule, dimension)` pair, and the position of the
|
|
1500
|
+
* FIRST occurrence seen (merging keeps the left operand's position, so on a
|
|
1501
|
+
* multi-file merge the first file that exhibited the spelling names it).
|
|
1502
|
+
*
|
|
1503
|
+
* @public
|
|
1504
|
+
*/
|
|
1505
|
+
declare class StyleVoteTally extends StyleVoteTally_base {}
|
|
1506
|
+
declare const StyleFloorTally_base: Schema.Class<StyleFloorTally, Schema.Struct<{
|
|
1507
|
+
readonly rule: Schema.String;
|
|
1508
|
+
readonly dimension: Schema.String;
|
|
1509
|
+
readonly value: Schema.Number;
|
|
1510
|
+
}>, {}>;
|
|
1511
|
+
/**
|
|
1512
|
+
* An accumulated {@link StyleFloor} for a `(rule, dimension)` pair: the
|
|
1513
|
+
* largest value the observed sources prove the limit is AT LEAST. Merging
|
|
1514
|
+
* takes the maximum. Floors are informational — never resolved into config
|
|
1515
|
+
* options (see {@link StyleFloor}).
|
|
1516
|
+
*
|
|
1517
|
+
* @public
|
|
1518
|
+
*/
|
|
1519
|
+
declare class StyleFloorTally extends StyleFloorTally_base {}
|
|
1520
|
+
declare const StyleEvidence_base: Schema.Class<StyleEvidence, Schema.Struct<{
|
|
1521
|
+
readonly votes: Schema.$Array<typeof StyleVoteTally>;
|
|
1522
|
+
readonly floors: Schema.$Array<typeof StyleFloorTally>;
|
|
1523
|
+
}>, {}>;
|
|
1524
|
+
/**
|
|
1525
|
+
* Per-dimension style evidence (#345): what the observed sources say about
|
|
1526
|
+
* each inferable `(rule, dimension)` — vote histograms with first-seen
|
|
1527
|
+
* positions, and measured floors.
|
|
1528
|
+
*
|
|
1529
|
+
* Evidence is a MONOID: {@link StyleEvidence.empty} is the identity and
|
|
1530
|
+
* {@link StyleEvidence.combine} is associative, so multi-file inference is
|
|
1531
|
+
* observe-per-file, merge, resolve — and the N-file loop stays the caller's
|
|
1532
|
+
* (no IO enters the package). `combine` normalizes: counts of the same
|
|
1533
|
+
* `(rule, dimension, value)` add, the left operand's first-seen position
|
|
1534
|
+
* wins, floors take the maximum, and the result is canonically sorted — so
|
|
1535
|
+
* every value `observe`/`combine`/`fromObservations` produces is canonical
|
|
1536
|
+
* and the monoid laws hold structurally over them.
|
|
1537
|
+
*
|
|
1538
|
+
* @public
|
|
1539
|
+
*/
|
|
1540
|
+
declare class StyleEvidence extends StyleEvidence_base {
|
|
1541
|
+
/** The monoid identity: no observations. */
|
|
1542
|
+
static readonly empty: StyleEvidence;
|
|
1543
|
+
/**
|
|
1544
|
+
* Merge two bodies of evidence (associative; {@link StyleEvidence.empty}
|
|
1545
|
+
* is the identity). Vote counts add, the left operand's first-seen
|
|
1546
|
+
* position wins per spelling, floors take the maximum.
|
|
1547
|
+
*/
|
|
1548
|
+
static combine(a: StyleEvidence, b: StyleEvidence): StyleEvidence;
|
|
1549
|
+
/**
|
|
1550
|
+
* Build canonical evidence from one rule's raw observations — the
|
|
1551
|
+
* homomorphism from observation lists into the monoid
|
|
1552
|
+
* (`fromObservations(r, [...a, ...b])` equals
|
|
1553
|
+
* `combine(fromObservations(r, a), fromObservations(r, b))`). `observe`
|
|
1554
|
+
* uses it per rule; it is public so custom tooling can construct evidence
|
|
1555
|
+
* without a {@link LintContext}.
|
|
1556
|
+
*/
|
|
1557
|
+
static fromObservations(rule: string, observations: Iterable<StyleObservation>): StyleEvidence;
|
|
1558
|
+
}
|
|
1559
|
+
declare const StyleConflict_base: Schema.Class<StyleConflict, Schema.Struct<{
|
|
1560
|
+
readonly rule: Schema.String;
|
|
1561
|
+
readonly dimension: Schema.String;
|
|
1562
|
+
readonly candidates: Schema.$Array<typeof StyleVoteTally>;
|
|
1563
|
+
}>, {}>;
|
|
1564
|
+
/**
|
|
1565
|
+
* One strict-resolution conflict: a `(rule, dimension)` whose observed
|
|
1566
|
+
* spellings disagree. `candidates` carries every spelling with its count and
|
|
1567
|
+
* first-seen position, ordered by count descending (dominant first).
|
|
1568
|
+
*
|
|
1569
|
+
* @public
|
|
1570
|
+
*/
|
|
1571
|
+
declare class StyleConflict extends StyleConflict_base {}
|
|
1572
|
+
declare const YamlStyleConflictError_base: Schema.Class<YamlStyleConflictError, Schema.TaggedStruct<"YamlStyleConflictError", {
|
|
1573
|
+
readonly conflicts: Schema.$Array<typeof StyleConflict>;
|
|
1574
|
+
}>, import("effect/Cause").YieldableError>;
|
|
1575
|
+
/**
|
|
1576
|
+
* Raised by strict config inference when observed evidence is not unanimous:
|
|
1577
|
+
* carries every conflicting `(rule, dimension)` as a structured
|
|
1578
|
+
* {@link StyleConflict} — dimension, all spellings, counts and positions —
|
|
1579
|
+
* never a collapsed `reason` string (the structure-preserving-errors house
|
|
1580
|
+
* rule). Unobserved dimensions never conflict: they fall back to the base
|
|
1581
|
+
* config's defaults.
|
|
1582
|
+
*
|
|
1583
|
+
* @public
|
|
1584
|
+
*/
|
|
1585
|
+
declare class YamlStyleConflictError extends YamlStyleConflictError_base {
|
|
1586
|
+
get message(): string;
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* A lenient inference report: the inferred config plus the residual — the
|
|
1590
|
+
* diagnostics that config still produces on the observed text ("here is your
|
|
1591
|
+
* config, and the places that do not match it").
|
|
1592
|
+
*
|
|
1593
|
+
* @public
|
|
1594
|
+
*/
|
|
1595
|
+
interface YamlLintInference {
|
|
1596
|
+
readonly config: YamlLintConfig;
|
|
1597
|
+
readonly residual: ReadonlyArray<YamlLintDiagnostic>;
|
|
1598
|
+
}
|
|
1402
1599
|
/**
|
|
1403
1600
|
* Linting statics. Not instantiable.
|
|
1404
1601
|
*
|
|
@@ -1447,6 +1644,52 @@ declare class YamlLint {
|
|
|
1447
1644
|
* is dropped (its diagnostic remains reported by {@link YamlLint.run}).
|
|
1448
1645
|
*/
|
|
1449
1646
|
static fix(text: string, rules: ReadonlyArray<YamlRule>, config: YamlLintConfig): Result.Result<string, YamlParseError>;
|
|
1647
|
+
/**
|
|
1648
|
+
* Observe the style `text` already follows (#345): run every rule's
|
|
1649
|
+
* optional `infer` hook over one eagerly-built context and merge the
|
|
1650
|
+
* observations into canonical {@link StyleEvidence}. Pure and total —
|
|
1651
|
+
* strings in, evidence out; observing N files is N `observe` calls merged
|
|
1652
|
+
* with {@link StyleEvidence.combine}, and the N-file loop stays the
|
|
1653
|
+
* caller's (no IO enters the package).
|
|
1654
|
+
*/
|
|
1655
|
+
static observe(text: string, rules: ReadonlyArray<YamlRule>): StyleEvidence;
|
|
1656
|
+
/**
|
|
1657
|
+
* Strict resolution: every OBSERVED dimension must be unanimous, and the
|
|
1658
|
+
* unanimous picks overlay `base` (default {@link YamlLintConfig.default})
|
|
1659
|
+
* into an exact config. Conflicting evidence fails with
|
|
1660
|
+
* {@link YamlStyleConflictError} naming every conflicting dimension, all
|
|
1661
|
+
* spellings, counts and first-seen positions. Unobserved is NOT
|
|
1662
|
+
* conflicting: a corpus with no comments says nothing about
|
|
1663
|
+
* `comments-spacing`, so that dimension falls back to `base` rather than
|
|
1664
|
+
* failing. A rule `base` sets to `"off"` stays off — an explicit disable
|
|
1665
|
+
* outranks inference.
|
|
1666
|
+
*/
|
|
1667
|
+
static resolveStrict(evidence: StyleEvidence, base?: YamlLintConfig): Result.Result<YamlLintConfig, YamlStyleConflictError>;
|
|
1668
|
+
/**
|
|
1669
|
+
* Lenient resolution: the dominant (plurality) spelling per observed
|
|
1670
|
+
* dimension overlays `base` (default {@link YamlLintConfig.default}); a
|
|
1671
|
+
* tie breaks deterministically to the first candidate in canonical
|
|
1672
|
+
* value order. Total — lenient resolution cannot fail; the places that do
|
|
1673
|
+
* not match the inferred config surface as the residual report (run the
|
|
1674
|
+
* lint with the inferred config, or use {@link YamlLint.inferLenient}).
|
|
1675
|
+
*/
|
|
1676
|
+
static resolveLenient(evidence: StyleEvidence, base?: YamlLintConfig): YamlLintConfig;
|
|
1677
|
+
/**
|
|
1678
|
+
* Single-text strict inference: `observe` then {@link YamlLint.resolveStrict}
|
|
1679
|
+
* in one step. Multi-file callers observe each file and merge with
|
|
1680
|
+
* {@link StyleEvidence.combine} before resolving.
|
|
1681
|
+
*/
|
|
1682
|
+
static inferStrict(text: string, rules: ReadonlyArray<YamlRule>, base?: YamlLintConfig): Result.Result<YamlLintConfig, YamlStyleConflictError>;
|
|
1683
|
+
/**
|
|
1684
|
+
* Single-text lenient inference: `observe`, resolve dominant picks over
|
|
1685
|
+
* `base`, then run the lint with the inferred config — returning the
|
|
1686
|
+
* config AND the residual diagnostics it still produces ("here is your
|
|
1687
|
+
* config, and the places that do not match it"). One context serves both
|
|
1688
|
+
* the observation and the residual run. Multi-file callers compose the
|
|
1689
|
+
* primitives instead: observe each file, {@link StyleEvidence.combine},
|
|
1690
|
+
* {@link YamlLint.resolveLenient}, then {@link YamlLint.run} per file.
|
|
1691
|
+
*/
|
|
1692
|
+
static inferLenient(text: string, rules: ReadonlyArray<YamlRule>, base?: YamlLintConfig): YamlLintInference;
|
|
1450
1693
|
}
|
|
1451
1694
|
//#endregion
|
|
1452
1695
|
//#region src/YamlVisitor.d.ts
|
|
@@ -2051,5 +2294,5 @@ declare class YamlVisitor {
|
|
|
2051
2294
|
static visit(text: string, options?: YamlParseOptions): Stream.Stream<YamlVisitorEvent>;
|
|
2052
2295
|
}
|
|
2053
2296
|
//#endregion
|
|
2054
|
-
export { CollectionStyle, type LintContext, type LintLine, QuoteStyle, ScalarChomp, ScalarStyle, Yaml, YamlAlias, type YamlAliasEncoded, type YamlBoundCodec, YamlComposerErrorCode, YamlDiagnostic, YamlDirective, YamlDocument, YamlEdit, YamlErrorCode, YamlFormat, YamlFormattingOptions, YamlLexErrorCode, YamlLint, YamlLintConfig, YamlLintDiagnostic, YamlLintRuleSetting, YamlLintSeverity, YamlMap, type YamlMapEncoded, YamlModificationError, YamlModifyErrorCode, YamlNode, YamlPair, YamlParseError, YamlParseErrorCode, YamlParseOptions, type YamlPath, YamlRange, type YamlRangeLike, type YamlRule, YamlScalar, type YamlScalarEncoded, type YamlSegment, YamlSeq, type YamlSeqEncoded, YamlStringifyError, YamlStringifyErrorCode, YamlStringifyOptions, YamlToken, YamlTokenKind, YamlTokens, YamlVisitor, YamlVisitorEvent };
|
|
2297
|
+
export { CollectionStyle, type LintContext, type LintLine, QuoteStyle, ScalarChomp, ScalarStyle, StyleConflict, StyleEvidence, StyleFloor, StyleFloorTally, type StyleObservation, StyleVote, StyleVoteTally, Yaml, YamlAlias, type YamlAliasEncoded, type YamlBoundCodec, YamlComposerErrorCode, YamlDiagnostic, YamlDirective, YamlDocument, YamlEdit, YamlErrorCode, YamlFormat, YamlFormattingOptions, YamlLexErrorCode, YamlLint, YamlLintConfig, YamlLintDiagnostic, type YamlLintInference, YamlLintRuleSetting, YamlLintSeverity, YamlMap, type YamlMapEncoded, YamlModificationError, YamlModifyErrorCode, YamlNode, YamlPair, YamlParseError, YamlParseErrorCode, YamlParseOptions, type YamlPath, YamlRange, type YamlRangeLike, type YamlRule, YamlScalar, type YamlScalarEncoded, type YamlSegment, YamlSeq, type YamlSeqEncoded, YamlStringifyError, YamlStringifyErrorCode, YamlStringifyOptions, YamlStyleConflictError, YamlToken, YamlTokenKind, YamlTokens, YamlVisitor, YamlVisitorEvent };
|
|
2055
2298
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -4,9 +4,9 @@ import { Yaml, YamlParseError, YamlParseOptions, YamlStringifyError, YamlStringi
|
|
|
4
4
|
import { YamlDirective, YamlDocument } from "./YamlDocument.js";
|
|
5
5
|
import { YamlEdit, YamlRange } from "./YamlEdit.js";
|
|
6
6
|
import { YamlFormat, YamlFormattingOptions, YamlModificationError } from "./YamlFormat.js";
|
|
7
|
-
import { YamlLintDiagnostic, YamlLintSeverity } from "./YamlLintRule.js";
|
|
7
|
+
import { StyleFloor, StyleVote, YamlLintDiagnostic, YamlLintSeverity } from "./YamlLintRule.js";
|
|
8
8
|
import { YamlToken, YamlTokenKind, YamlTokens } from "./YamlToken.js";
|
|
9
|
-
import { YamlLint, YamlLintConfig, YamlLintRuleSetting } from "./YamlLint.js";
|
|
9
|
+
import { StyleConflict, StyleEvidence, StyleFloorTally, StyleVoteTally, YamlLint, YamlLintConfig, YamlLintRuleSetting, YamlStyleConflictError } from "./YamlLint.js";
|
|
10
10
|
import { YamlVisitor, YamlVisitorEvent } from "./YamlVisitor.js";
|
|
11
11
|
|
|
12
|
-
export { CollectionStyle, QuoteStyle, ScalarChomp, ScalarStyle, Yaml, YamlAlias, YamlComposerErrorCode, YamlDiagnostic, YamlDirective, YamlDocument, YamlEdit, YamlErrorCode, YamlFormat, YamlFormattingOptions, YamlLexErrorCode, YamlLint, YamlLintConfig, YamlLintDiagnostic, YamlLintRuleSetting, YamlLintSeverity, YamlMap, YamlModificationError, YamlModifyErrorCode, YamlNode, YamlPair, YamlParseError, YamlParseErrorCode, YamlParseOptions, YamlRange, YamlScalar, YamlSeq, YamlStringifyError, YamlStringifyErrorCode, YamlStringifyOptions, YamlToken, YamlTokenKind, YamlTokens, YamlVisitor, YamlVisitorEvent };
|
|
12
|
+
export { CollectionStyle, QuoteStyle, ScalarChomp, ScalarStyle, StyleConflict, StyleEvidence, StyleFloor, StyleFloorTally, StyleVote, StyleVoteTally, Yaml, YamlAlias, YamlComposerErrorCode, YamlDiagnostic, YamlDirective, YamlDocument, YamlEdit, YamlErrorCode, YamlFormat, YamlFormattingOptions, YamlLexErrorCode, YamlLint, YamlLintConfig, YamlLintDiagnostic, YamlLintRuleSetting, YamlLintSeverity, YamlMap, YamlModificationError, YamlModifyErrorCode, YamlNode, YamlPair, YamlParseError, YamlParseErrorCode, YamlParseOptions, YamlRange, YamlScalar, YamlSeq, YamlStringifyError, YamlStringifyErrorCode, YamlStringifyOptions, YamlStyleConflictError, YamlToken, YamlTokenKind, YamlTokens, YamlVisitor, YamlVisitorEvent };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isControlChar } from "./fold.js";
|
|
2
|
+
import { renderDoubleQuoted, renderSingleQuoted } from "./stringifier.js";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/requote.ts
|
|
5
|
+
/**
|
|
6
|
+
* True when `value` can be carried by a single-quoted flow scalar on one
|
|
7
|
+
* line. Single-quoted style has exactly one escape (`''`), so any character
|
|
8
|
+
* double-quote escape sequences exist to encode — newline, carriage return,
|
|
9
|
+
* tab, C0 controls, DEL and the C1 range — has no single-quoted spelling and
|
|
10
|
+
* makes the conversion impossible rather than lossy-but-tempting.
|
|
11
|
+
*/
|
|
12
|
+
function isSingleQuotable(value) {
|
|
13
|
+
for (let i = 0; i < value.length; i++) {
|
|
14
|
+
const code = value.charCodeAt(i);
|
|
15
|
+
if (code === 9 || code === 10 || code === 13) return false;
|
|
16
|
+
if (isControlChar(code)) return false;
|
|
17
|
+
if (code === 127 || code >= 128 && code <= 159) return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The replacement raw text that re-quotes `scalar` with `quote`, or
|
|
23
|
+
* `undefined` when no value-preserving replacement exists under `mode`
|
|
24
|
+
* (skipping is always correct; corrupting never is).
|
|
25
|
+
*/
|
|
26
|
+
function requoteScalarText(text, scalar, quote, mode) {
|
|
27
|
+
if (scalar.tag !== void 0 || scalar.anchor !== void 0) return void 0;
|
|
28
|
+
if (typeof scalar.value !== "string") return void 0;
|
|
29
|
+
const raw = text.slice(scalar.offset, scalar.offset + scalar.length);
|
|
30
|
+
if (raw.includes("\n") || raw.includes("\r")) return void 0;
|
|
31
|
+
if (mode === "conservative") {
|
|
32
|
+
const inner = scalar.style === "plain" ? raw : raw.slice(1, -1);
|
|
33
|
+
if (inner !== scalar.value) return void 0;
|
|
34
|
+
if (inner.includes(quote)) return void 0;
|
|
35
|
+
if (quote === "\"" && inner.includes("\\")) return void 0;
|
|
36
|
+
return `${quote}${inner}${quote}`;
|
|
37
|
+
}
|
|
38
|
+
if (quote === "\"") {
|
|
39
|
+
if (scalar.style !== "single-quoted") return void 0;
|
|
40
|
+
return renderDoubleQuoted(scalar.value);
|
|
41
|
+
}
|
|
42
|
+
if (scalar.style !== "double-quoted") return void 0;
|
|
43
|
+
if (!isSingleQuotable(scalar.value)) return void 0;
|
|
44
|
+
return renderSingleQuoted(scalar.value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { requoteScalarText };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
-
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
2
|
+
import { StyleVote, YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
3
|
import { nonNegativeIntegerOption } from "./util.js";
|
|
4
4
|
import { Schema } from "effect";
|
|
5
5
|
|
|
@@ -14,6 +14,25 @@ const commentsSpacingOptions = Schema.Struct({
|
|
|
14
14
|
minSpacesBefore: Schema.optionalKey(nonNegativeIntegerOption),
|
|
15
15
|
requireSpaceAfter: Schema.optionalKey(Schema.Boolean)
|
|
16
16
|
});
|
|
17
|
+
/**
|
|
18
|
+
* The horizontal whitespace run directly before a comment token, and whether
|
|
19
|
+
* line content precedes it (a TRAILING comment) — shared by the check and
|
|
20
|
+
* the inference hook so the two cannot disagree about what "before" means.
|
|
21
|
+
*/
|
|
22
|
+
const spacingBefore = (ctx, token) => {
|
|
23
|
+
let i = token.offset - 1;
|
|
24
|
+
let spaces = 0;
|
|
25
|
+
while (i >= 0 && (ctx.text[i] === " " || ctx.text[i] === " ")) {
|
|
26
|
+
spaces++;
|
|
27
|
+
i--;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
spaces,
|
|
31
|
+
hasContentBefore: i >= 0 && ctx.text[i] !== "\n" && ctx.text[i] !== "\r"
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/** Shebang exemption: `#!` at the very start of the stream. */
|
|
35
|
+
const isShebang = (token) => token.offset === 0 && token.text.startsWith("#!");
|
|
17
36
|
/** Comment spacing: after the `#`, and before a trailing comment's `#`. */
|
|
18
37
|
const commentsSpacing = {
|
|
19
38
|
id: "comments-spacing",
|
|
@@ -24,7 +43,7 @@ const commentsSpacing = {
|
|
|
24
43
|
const out = [];
|
|
25
44
|
for (const token of ctx.tokens) {
|
|
26
45
|
if (token.kind !== "comment") continue;
|
|
27
|
-
if (token
|
|
46
|
+
if (isShebang(token)) continue;
|
|
28
47
|
if (requireAfter && token.text.length > 1 && !/[ \t]/.test(token.text[1])) out.push(new YamlLintDiagnostic({
|
|
29
48
|
rule: "comments-spacing",
|
|
30
49
|
severity: "error",
|
|
@@ -39,13 +58,8 @@ const commentsSpacing = {
|
|
|
39
58
|
content: " "
|
|
40
59
|
})
|
|
41
60
|
}));
|
|
42
|
-
|
|
43
|
-
|
|
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({
|
|
61
|
+
const { spaces, hasContentBefore } = spacingBefore(ctx, token);
|
|
62
|
+
if (hasContentBefore && spaces < minBefore) out.push(new YamlLintDiagnostic({
|
|
49
63
|
rule: "comments-spacing",
|
|
50
64
|
severity: "error",
|
|
51
65
|
message: `Too few spaces before comment (${spaces} < ${minBefore})`,
|
|
@@ -61,6 +75,31 @@ const commentsSpacing = {
|
|
|
61
75
|
}));
|
|
62
76
|
}
|
|
63
77
|
return out;
|
|
78
|
+
},
|
|
79
|
+
infer: (ctx) => {
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const token of ctx.tokens) {
|
|
82
|
+
if (token.kind !== "comment") continue;
|
|
83
|
+
if (isShebang(token)) continue;
|
|
84
|
+
if (token.text.length > 1) out.push(StyleVote.make({
|
|
85
|
+
dimension: "requireSpaceAfter",
|
|
86
|
+
value: /[ \t]/.test(token.text[1]),
|
|
87
|
+
offset: token.offset,
|
|
88
|
+
length: token.length,
|
|
89
|
+
line: token.line,
|
|
90
|
+
character: token.character
|
|
91
|
+
}));
|
|
92
|
+
const { spaces, hasContentBefore } = spacingBefore(ctx, token);
|
|
93
|
+
if (hasContentBefore) out.push(StyleVote.make({
|
|
94
|
+
dimension: "minSpacesBefore",
|
|
95
|
+
value: spaces,
|
|
96
|
+
offset: token.offset - spaces,
|
|
97
|
+
length: spaces,
|
|
98
|
+
line: token.line,
|
|
99
|
+
character: token.character - spaces
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
64
103
|
}
|
|
65
104
|
};
|
|
66
105
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
-
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
2
|
+
import { StyleVote, YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
3
|
import { Schema } from "effect";
|
|
4
4
|
|
|
5
5
|
//#region src/internal/rules/document-end.ts
|
|
@@ -14,31 +14,43 @@ const TRIVIA = /* @__PURE__ */ new Set([
|
|
|
14
14
|
"comment",
|
|
15
15
|
"byte-order-mark"
|
|
16
16
|
]);
|
|
17
|
+
/** The last non-trivia token: it decides whether the stream ends with `...`. */
|
|
18
|
+
const tailToken = (ctx) => [...ctx.tokens].reverse().find((t) => !TRIVIA.has(t.kind));
|
|
19
|
+
/**
|
|
20
|
+
* The end-of-stream position, shared by the missing-marker diagnostic and
|
|
21
|
+
* the absent-marker vote so the two cannot drift apart. `line`/`character`
|
|
22
|
+
* must agree with `offset` (= text length): when the text ends with a
|
|
23
|
+
* newline, that offset sits at the head of the line AFTER the last content
|
|
24
|
+
* line `buildLines` kept.
|
|
25
|
+
*/
|
|
26
|
+
const endOfStreamPosition = (ctx) => {
|
|
27
|
+
const lastLine = ctx.lines[ctx.lines.length - 1];
|
|
28
|
+
const trailingNewline = ctx.text.endsWith("\n");
|
|
29
|
+
return {
|
|
30
|
+
offset: ctx.text.length,
|
|
31
|
+
length: 0,
|
|
32
|
+
line: (lastLine?.number ?? 0) + (trailingNewline ? 1 : 0),
|
|
33
|
+
character: trailingNewline ? 0 : lastLine?.text.length ?? 0
|
|
34
|
+
};
|
|
35
|
+
};
|
|
17
36
|
/** The `...` marker at the tail of the stream. */
|
|
18
37
|
const documentEnd = {
|
|
19
38
|
id: "document-end",
|
|
20
39
|
check: (ctx, options) => {
|
|
21
40
|
const present = options?.present ?? true;
|
|
22
|
-
const tail =
|
|
41
|
+
const tail = tailToken(ctx);
|
|
23
42
|
const ended = tail?.kind === "document-end";
|
|
24
|
-
if (present && !ended && ctx.text.trim() !== "") {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
message: "Missing \"...\" document end marker",
|
|
43
|
+
if (present && !ended && ctx.text.trim() !== "") return [new YamlLintDiagnostic({
|
|
44
|
+
rule: "document-end",
|
|
45
|
+
severity: "error",
|
|
46
|
+
message: "Missing \"...\" document end marker",
|
|
47
|
+
...endOfStreamPosition(ctx),
|
|
48
|
+
fix: YamlEdit.make({
|
|
31
49
|
offset: ctx.text.length,
|
|
32
50
|
length: 0,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
offset: ctx.text.length,
|
|
37
|
-
length: 0,
|
|
38
|
-
content: ctx.text.endsWith("\n") ? "...\n" : "\n...\n"
|
|
39
|
-
})
|
|
40
|
-
})];
|
|
41
|
-
}
|
|
51
|
+
content: ctx.text.endsWith("\n") ? "...\n" : "\n...\n"
|
|
52
|
+
})
|
|
53
|
+
})];
|
|
42
54
|
if (!present && tail !== void 0 && tail.kind === "document-end") {
|
|
43
55
|
const alone = (ctx.lines[tail.line]?.text ?? "").trim() === "...";
|
|
44
56
|
const terminator = ctx.text.startsWith("\r\n", tail.offset + tail.length) ? 2 : 1;
|
|
@@ -58,6 +70,24 @@ const documentEnd = {
|
|
|
58
70
|
})];
|
|
59
71
|
}
|
|
60
72
|
return [];
|
|
73
|
+
},
|
|
74
|
+
infer: (ctx) => {
|
|
75
|
+
if (ctx.text.trim() === "") return [];
|
|
76
|
+
const tail = tailToken(ctx);
|
|
77
|
+
if (tail === void 0) return [];
|
|
78
|
+
if (tail.kind === "document-end") return [StyleVote.make({
|
|
79
|
+
dimension: "present",
|
|
80
|
+
value: true,
|
|
81
|
+
offset: tail.offset,
|
|
82
|
+
length: tail.length,
|
|
83
|
+
line: tail.line,
|
|
84
|
+
character: tail.character
|
|
85
|
+
})];
|
|
86
|
+
return [StyleVote.make({
|
|
87
|
+
dimension: "present",
|
|
88
|
+
value: false,
|
|
89
|
+
...endOfStreamPosition(ctx)
|
|
90
|
+
})];
|
|
61
91
|
}
|
|
62
92
|
};
|
|
63
93
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
-
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
2
|
+
import { StyleVote, YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
3
|
import { Schema } from "effect";
|
|
4
4
|
|
|
5
5
|
//#region src/internal/rules/document-start.ts
|
|
@@ -15,13 +15,17 @@ const TRIVIA = /* @__PURE__ */ new Set([
|
|
|
15
15
|
"byte-order-mark",
|
|
16
16
|
"directive"
|
|
17
17
|
]);
|
|
18
|
+
/** True when the stream carries `%YAML`/`%TAG` directives — which REQUIRE `---`. */
|
|
19
|
+
const hasDirectives = (ctx) => ctx.tokens.some((t) => t.kind === "directive");
|
|
20
|
+
/** The first non-trivia token: it decides whether the stream is headed by `---`. */
|
|
21
|
+
const headToken = (ctx) => ctx.tokens.find((t) => !TRIVIA.has(t.kind));
|
|
18
22
|
/** The `---` marker at the head of the stream. */
|
|
19
23
|
const documentStart = {
|
|
20
24
|
id: "document-start",
|
|
21
25
|
check: (ctx, options) => {
|
|
22
26
|
const present = options?.present ?? true;
|
|
23
|
-
const
|
|
24
|
-
const first = ctx
|
|
27
|
+
const directives = hasDirectives(ctx);
|
|
28
|
+
const first = headToken(ctx);
|
|
25
29
|
const headed = first?.kind === "document-start";
|
|
26
30
|
if (present && !headed && ctx.text.trim() !== "") return [new YamlLintDiagnostic({
|
|
27
31
|
rule: "document-start",
|
|
@@ -31,7 +35,7 @@ const documentStart = {
|
|
|
31
35
|
length: 0,
|
|
32
36
|
line: 0,
|
|
33
37
|
character: 0,
|
|
34
|
-
...
|
|
38
|
+
...directives ? {} : { fix: YamlEdit.make({
|
|
35
39
|
offset: 0,
|
|
36
40
|
length: 0,
|
|
37
41
|
content: "---\n"
|
|
@@ -39,7 +43,7 @@ const documentStart = {
|
|
|
39
43
|
})];
|
|
40
44
|
if (!present && headed && first !== void 0) {
|
|
41
45
|
const lineText = ctx.lines[first.line]?.text ?? "";
|
|
42
|
-
const alone = !
|
|
46
|
+
const alone = !directives && lineText.trim() === "---";
|
|
43
47
|
const terminator = ctx.text.startsWith("\r\n", first.offset + first.length) ? 2 : 1;
|
|
44
48
|
return [new YamlLintDiagnostic({
|
|
45
49
|
rule: "document-start",
|
|
@@ -57,6 +61,28 @@ const documentStart = {
|
|
|
57
61
|
})];
|
|
58
62
|
}
|
|
59
63
|
return [];
|
|
64
|
+
},
|
|
65
|
+
infer: (ctx) => {
|
|
66
|
+
if (ctx.text.trim() === "") return [];
|
|
67
|
+
const first = headToken(ctx);
|
|
68
|
+
if (first === void 0) return [];
|
|
69
|
+
const headed = first.kind === "document-start";
|
|
70
|
+
if (!headed && hasDirectives(ctx)) return [];
|
|
71
|
+
return [StyleVote.make(headed ? {
|
|
72
|
+
dimension: "present",
|
|
73
|
+
value: true,
|
|
74
|
+
offset: first.offset,
|
|
75
|
+
length: first.length,
|
|
76
|
+
line: first.line,
|
|
77
|
+
character: first.character
|
|
78
|
+
} : {
|
|
79
|
+
dimension: "present",
|
|
80
|
+
value: false,
|
|
81
|
+
offset: 0,
|
|
82
|
+
length: 0,
|
|
83
|
+
line: 0,
|
|
84
|
+
character: 0
|
|
85
|
+
})];
|
|
60
86
|
}
|
|
61
87
|
};
|
|
62
88
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
-
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
2
|
+
import { StyleFloor, YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
3
|
import { insideScalarSpan, nonNegativeIntegerOption } from "./util.js";
|
|
4
4
|
import { Schema } from "effect";
|
|
5
5
|
|
|
@@ -63,6 +63,26 @@ const emptyLines = {
|
|
|
63
63
|
i = end;
|
|
64
64
|
}
|
|
65
65
|
return out;
|
|
66
|
+
},
|
|
67
|
+
infer: (ctx) => {
|
|
68
|
+
let longest = 0;
|
|
69
|
+
const lines = ctx.lines;
|
|
70
|
+
let i = 0;
|
|
71
|
+
while (i < lines.length) {
|
|
72
|
+
const line = lines[i];
|
|
73
|
+
if (line === void 0 || line.text !== "" || insideScalarSpan(ctx.tokens, line.offset)) {
|
|
74
|
+
i++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
let end = i;
|
|
78
|
+
while (end < lines.length && (lines[end]?.text ?? "x") === "") end++;
|
|
79
|
+
if (i !== 0 && end !== lines.length && end - i > longest) longest = end - i;
|
|
80
|
+
i = end;
|
|
81
|
+
}
|
|
82
|
+
return longest > 0 ? [StyleFloor.make({
|
|
83
|
+
dimension: "max",
|
|
84
|
+
value: longest
|
|
85
|
+
})] : [];
|
|
66
86
|
}
|
|
67
87
|
};
|
|
68
88
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
1
|
+
import { StyleVote, YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
2
2
|
import { coveringToken, isScalarContinuationLine, nonNegativeIntegerOption } from "./util.js";
|
|
3
3
|
import { Schema } from "effect";
|
|
4
4
|
|
|
@@ -30,6 +30,44 @@ const flowDepthAtLineStarts = (ctx) => {
|
|
|
30
30
|
}
|
|
31
31
|
return depths;
|
|
32
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* The content lines block structure speaks about — shared by the check and
|
|
35
|
+
* the inference hook so the two read the same lines. Skips blanks, comment
|
|
36
|
+
* lines, directives, tab-indented lines (the parser's error), lines inside
|
|
37
|
+
* flow collections, scalar continuations and document markers.
|
|
38
|
+
*/
|
|
39
|
+
const contentLines = (ctx) => {
|
|
40
|
+
const flowDepths = flowDepthAtLineStarts(ctx);
|
|
41
|
+
const content = [];
|
|
42
|
+
for (const line of ctx.lines) {
|
|
43
|
+
const indent = line.text.length - line.text.trimStart().length;
|
|
44
|
+
const firstChar = line.text[indent];
|
|
45
|
+
if (firstChar === void 0) continue;
|
|
46
|
+
if (firstChar === "#") continue;
|
|
47
|
+
if (firstChar === "%") continue;
|
|
48
|
+
if (line.text.slice(0, indent).includes(" ")) continue;
|
|
49
|
+
if ((flowDepths[line.number] ?? 0) > 0) continue;
|
|
50
|
+
if (isScalarContinuationLine(ctx.tokens, line.offset, line.offset + indent)) continue;
|
|
51
|
+
if (line.text.slice(indent).startsWith("---") || line.text.slice(indent).startsWith("...")) continue;
|
|
52
|
+
content.push({
|
|
53
|
+
line,
|
|
54
|
+
indent,
|
|
55
|
+
firstChar
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return content;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* True when consecutive content lines `prev` → `curr` are a mapping key
|
|
62
|
+
* followed by a sequence entry — the pair the `indentSequences` policy is
|
|
63
|
+
* about. Shared by the check and the inference hook.
|
|
64
|
+
*/
|
|
65
|
+
const isKeyThenSeqEntry = (ctx, prev, curr, unit) => {
|
|
66
|
+
if (curr.firstChar !== "-") return false;
|
|
67
|
+
if (coveringToken(ctx.tokens, curr.line.offset + curr.indent)?.kind !== "block-seq-entry") return false;
|
|
68
|
+
if (!prev.line.text.replace(/ #.*$/, "").trimEnd().endsWith(":")) return false;
|
|
69
|
+
return curr.indent === prev.indent || curr.indent === prev.indent + (unit ?? curr.indent - prev.indent);
|
|
70
|
+
};
|
|
33
71
|
/** Block-structure indent style. */
|
|
34
72
|
const indentation = {
|
|
35
73
|
id: "indentation",
|
|
@@ -38,24 +76,7 @@ const indentation = {
|
|
|
38
76
|
const spacesOpt = opts.spaces ?? "consistent";
|
|
39
77
|
const seqOpt = opts.indentSequences ?? "consistent";
|
|
40
78
|
const out = [];
|
|
41
|
-
const
|
|
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
|
-
}
|
|
79
|
+
const content = contentLines(ctx);
|
|
59
80
|
let unit = typeof spacesOpt === "number" ? spacesOpt : void 0;
|
|
60
81
|
const stack = [0];
|
|
61
82
|
for (const { line, indent } of content) {
|
|
@@ -79,10 +100,7 @@ const indentation = {
|
|
|
79
100
|
for (let i = 1; i < content.length; i++) {
|
|
80
101
|
const prev = content[i - 1];
|
|
81
102
|
const curr = content[i];
|
|
82
|
-
if (curr
|
|
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;
|
|
103
|
+
if (!isKeyThenSeqEntry(ctx, prev, curr, unit)) continue;
|
|
86
104
|
const indented = curr.indent > prev.indent;
|
|
87
105
|
if (seqIndented === void 0) seqIndented = indented;
|
|
88
106
|
else if (indented !== seqIndented) out.push(new YamlLintDiagnostic({
|
|
@@ -96,6 +114,41 @@ const indentation = {
|
|
|
96
114
|
}));
|
|
97
115
|
}
|
|
98
116
|
return out;
|
|
117
|
+
},
|
|
118
|
+
infer: (ctx) => {
|
|
119
|
+
const out = [];
|
|
120
|
+
const content = contentLines(ctx);
|
|
121
|
+
let unit;
|
|
122
|
+
const stack = [0];
|
|
123
|
+
for (const { line, indent } of content) {
|
|
124
|
+
const top = stack[stack.length - 1];
|
|
125
|
+
if (indent > top) {
|
|
126
|
+
out.push(StyleVote.make({
|
|
127
|
+
dimension: "spaces",
|
|
128
|
+
value: indent - top,
|
|
129
|
+
offset: line.offset,
|
|
130
|
+
length: indent,
|
|
131
|
+
line: line.number,
|
|
132
|
+
character: 0
|
|
133
|
+
}));
|
|
134
|
+
if (unit === void 0) unit = indent - top;
|
|
135
|
+
stack.push(indent);
|
|
136
|
+
} else if (indent < top) while (stack.length > 1 && stack[stack.length - 1] > indent) stack.pop();
|
|
137
|
+
}
|
|
138
|
+
for (let i = 1; i < content.length; i++) {
|
|
139
|
+
const prev = content[i - 1];
|
|
140
|
+
const curr = content[i];
|
|
141
|
+
if (!isKeyThenSeqEntry(ctx, prev, curr, unit)) continue;
|
|
142
|
+
out.push(StyleVote.make({
|
|
143
|
+
dimension: "indentSequences",
|
|
144
|
+
value: curr.indent > prev.indent,
|
|
145
|
+
offset: curr.line.offset,
|
|
146
|
+
length: curr.indent,
|
|
147
|
+
line: curr.line.number,
|
|
148
|
+
character: 0
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
99
152
|
}
|
|
100
153
|
};
|
|
101
154
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
1
|
+
import { StyleFloor, YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
2
2
|
import { nonNegativeIntegerOption } from "./util.js";
|
|
3
3
|
import { Schema } from "effect";
|
|
4
4
|
|
|
@@ -29,6 +29,14 @@ const lineLength = {
|
|
|
29
29
|
character: max
|
|
30
30
|
}));
|
|
31
31
|
return out;
|
|
32
|
+
},
|
|
33
|
+
infer: (ctx) => {
|
|
34
|
+
let longest = 0;
|
|
35
|
+
for (const line of ctx.lines) if (line.text.length > longest) longest = line.text.length;
|
|
36
|
+
return longest > 0 ? [StyleFloor.make({
|
|
37
|
+
dimension: "max",
|
|
38
|
+
value: longest
|
|
39
|
+
})] : [];
|
|
32
40
|
}
|
|
33
41
|
};
|
|
34
42
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { YamlEdit } from "../../YamlEdit.js";
|
|
2
|
-
import {
|
|
2
|
+
import { requoteScalarText } from "../requote.js";
|
|
3
|
+
import { StyleVote, YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
|
|
3
4
|
import { positionAt, walkScalars } from "./util.js";
|
|
4
5
|
import { Schema } from "effect";
|
|
5
6
|
|
|
@@ -14,20 +15,19 @@ const quotedStringsOptions = Schema.Struct({
|
|
|
14
15
|
quoteType: Schema.optionalKey(Schema.Literals(["single", "double"])),
|
|
15
16
|
required: Schema.optionalKey(Schema.Boolean)
|
|
16
17
|
});
|
|
17
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* A value-preserving requote/wrap edit, or undefined when none is safe.
|
|
20
|
+
* Delegates to the shared helper's CONSERVATIVE mode (#347) — the shipped
|
|
21
|
+
* fix behavior stays exactly as released; the escaping-capable mode belongs
|
|
22
|
+
* to the format path's opt-in `requoteScalars`, not the lint fix.
|
|
23
|
+
*/
|
|
18
24
|
const safeQuoteFix = (ctx, scalar, quote) => {
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
const raw = ctx.text.slice(scalar.offset, scalar.offset + scalar.length);
|
|
22
|
-
if (raw.includes("\n")) return void 0;
|
|
23
|
-
const inner = scalar.style === "plain" ? raw : raw.slice(1, -1);
|
|
24
|
-
if (inner !== scalar.value) return void 0;
|
|
25
|
-
if (inner.includes(quote)) return void 0;
|
|
26
|
-
if (quote === "\"" && inner.includes("\\")) return void 0;
|
|
25
|
+
const content = requoteScalarText(ctx.text, scalar, quote, "conservative");
|
|
26
|
+
if (content === void 0) return void 0;
|
|
27
27
|
return YamlEdit.make({
|
|
28
28
|
offset: scalar.offset,
|
|
29
29
|
length: scalar.length,
|
|
30
|
-
content
|
|
30
|
+
content
|
|
31
31
|
});
|
|
32
32
|
};
|
|
33
33
|
/** Quote-style policy for string value scalars. */
|
|
@@ -74,6 +74,24 @@ const quotedStrings = {
|
|
|
74
74
|
}
|
|
75
75
|
});
|
|
76
76
|
return out;
|
|
77
|
+
},
|
|
78
|
+
infer: (ctx) => {
|
|
79
|
+
const out = [];
|
|
80
|
+
walkScalars(ctx.document.contents, "root", (scalar, role) => {
|
|
81
|
+
if (role === "key") return;
|
|
82
|
+
if (typeof scalar.value !== "string") return;
|
|
83
|
+
if (scalar.style !== "single-quoted" && scalar.style !== "double-quoted") return;
|
|
84
|
+
const pos = positionAt(ctx.lines, scalar.offset);
|
|
85
|
+
out.push(StyleVote.make({
|
|
86
|
+
dimension: "quoteType",
|
|
87
|
+
value: scalar.style === "single-quoted" ? "single" : "double",
|
|
88
|
+
offset: scalar.offset,
|
|
89
|
+
length: scalar.length,
|
|
90
|
+
line: pos.line,
|
|
91
|
+
character: pos.character
|
|
92
|
+
}));
|
|
93
|
+
});
|
|
94
|
+
return out;
|
|
77
95
|
}
|
|
78
96
|
};
|
|
79
97
|
|
package/internal/stringifier.js
CHANGED
|
@@ -1134,4 +1134,4 @@ function stringifyDocument(doc, options) {
|
|
|
1134
1134
|
}
|
|
1135
1135
|
|
|
1136
1136
|
//#endregion
|
|
1137
|
-
export { StringifyDepthExceeded, StringifyFailure, stringifyDocument, stringifyValue, stripNodeComments };
|
|
1137
|
+
export { StringifyDepthExceeded, StringifyFailure, renderDoubleQuoted, renderSingleQuoted, stringifyDocument, stringifyValue, stripNodeComments };
|
package/package.json
CHANGED