@markuplint/ml-core 5.0.0-rc.4 → 5.0.0-rc.6
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/CHANGELOG.md +56 -0
- package/README.md +0 -5
- package/lib/cursor-offset.js +0 -3
- package/lib/fix-applier.js +3 -9
- package/lib/ml-core.d.ts +33 -3
- package/lib/ml-core.js +276 -62
- package/lib/ml-dom/helper/accname.d.ts +0 -8
- package/lib/ml-dom/helper/accname.js +7 -10
- package/lib/ml-dom/node/attr.js +3 -1
- package/lib/ml-dom/node/block.d.ts +6 -0
- package/lib/ml-dom/node/block.js +6 -0
- package/lib/ml-dom/node/child-node.d.ts +0 -9
- package/lib/ml-dom/node/child-node.js +0 -9
- package/lib/ml-dom/node/document.d.ts +20 -1
- package/lib/ml-dom/node/document.js +24 -13
- package/lib/ml-dom/node/element-close-tag.d.ts +12 -0
- package/lib/ml-dom/node/element-close-tag.js +12 -0
- package/lib/ml-dom/node/element.d.ts +22 -0
- package/lib/ml-dom/node/element.js +37 -11
- package/lib/ml-dom/node/node-store.d.ts +0 -3
- package/lib/ml-dom/node/node-store.js +0 -3
- package/lib/ml-dom/node/node.d.ts +34 -1
- package/lib/ml-dom/node/node.js +34 -16
- package/lib/ml-dom/node/parent-node.js +0 -6
- package/lib/ml-dom/node/rule-mapper.d.ts +8 -0
- package/lib/ml-dom/node/rule-mapper.js +8 -0
- package/lib/ml-rule/ml-rule.d.ts +19 -0
- package/lib/ml-rule/ml-rule.js +38 -7
- package/lib/ml-rule/types.d.ts +110 -1
- package/lib/ml-rule/types.js +28 -1
- package/lib/ruleset/index.d.ts +2 -1
- package/lib/ruleset/index.js +2 -1
- package/lib/test/index.js +1 -1
- package/lib/types.d.ts +8 -1
- package/lib/virtual-rule.d.ts +10 -0
- package/lib/virtual-rule.js +1 -24
- package/package.json +13 -13
- package/ARCHITECTURE.ja.md +0 -676
- package/ARCHITECTURE.md +0 -726
- package/SKILL.md +0 -61
- package/docs/linting-pipeline.ja.md +0 -307
- package/docs/linting-pipeline.md +0 -307
- package/docs/maintenance.ja.md +0 -210
- package/docs/maintenance.md +0 -210
- package/docs/ml-dom/attr.ja.md +0 -103
- package/docs/ml-dom/attr.md +0 -103
- package/docs/ml-dom/block.ja.md +0 -272
- package/docs/ml-dom/block.md +0 -272
- package/docs/ml-dom/document.ja.md +0 -134
- package/docs/ml-dom/document.md +0 -134
- package/docs/ml-dom/element.ja.md +0 -161
- package/docs/ml-dom/element.md +0 -161
- package/docs/ml-dom/helpers.ja.md +0 -203
- package/docs/ml-dom/helpers.md +0 -203
- package/docs/ml-dom/node.ja.md +0 -199
- package/docs/ml-dom/node.md +0 -199
- package/docs/ml-dom/others.ja.md +0 -120
- package/docs/ml-dom/others.md +0 -120
- package/docs/ml-dom/overview.ja.md +0 -102
- package/docs/ml-dom/overview.md +0 -102
- package/docs/ml-dom/pretender.ja.md +0 -269
- package/docs/ml-dom/pretender.md +0 -269
- package/docs/ml-dom/rule-mapping.ja.md +0 -371
- package/docs/ml-dom/rule-mapping.md +0 -371
- package/docs/ml-dom.ja.md +0 -18
- package/docs/ml-dom.md +0 -18
- package/docs/rule-system.ja.md +0 -287
- package/docs/rule-system.md +0 -287
package/lib/ml-core.js
CHANGED
|
@@ -4,6 +4,20 @@ import { applyFixes } from './fix-applier.js';
|
|
|
4
4
|
import { Document } from './ml-dom/index.js';
|
|
5
5
|
import { expandNamedNodeRules, expandNamedRules } from './virtual-rule.js';
|
|
6
6
|
const resultLog = log.extend('result');
|
|
7
|
+
/**
|
|
8
|
+
* `ruleId` of a genuinely broken config (unresolved rule reference, plugin
|
|
9
|
+
* resolution failure, ...). Exported so consumers that need to recognize
|
|
10
|
+
* config-level violations (e.g. the CLI's per-run dedupe and failed-file
|
|
11
|
+
* counting in `packages/markuplint/src/cli/command.ts`) reference the same
|
|
12
|
+
* literal `MLCore.verify()` emits, instead of duplicating the string.
|
|
13
|
+
*/
|
|
14
|
+
export const CONFIG_ERROR_RULE_ID = 'config-error';
|
|
15
|
+
/**
|
|
16
|
+
* `ruleId` of a deprecated-but-working rule name notice — see
|
|
17
|
+
* {@link CONFIG_ERROR_RULE_ID} for why this is exported rather than a
|
|
18
|
+
* private literal.
|
|
19
|
+
*/
|
|
20
|
+
export const RULE_DEPRECATION_RULE_ID = 'rule-deprecation';
|
|
7
21
|
/**
|
|
8
22
|
* The core linting engine for markuplint.
|
|
9
23
|
*
|
|
@@ -24,7 +38,24 @@ export class MLCore {
|
|
|
24
38
|
#schemas;
|
|
25
39
|
#ruleCommonSettings;
|
|
26
40
|
#sourceCode;
|
|
41
|
+
/**
|
|
42
|
+
* Config-time errors (named-rule expansion, the `configErrors` fabric input).
|
|
43
|
+
* Set once per construction/`update()` and never mutated by re-parsing.
|
|
44
|
+
*/
|
|
27
45
|
#configErrors;
|
|
46
|
+
/**
|
|
47
|
+
* Deprecated-rule-name notices, kept structured and separate from
|
|
48
|
+
* `#configErrors` so `verify()` can report them under their own
|
|
49
|
+
* `rule-deprecation` ruleId. Set once per construction/`update()`, same
|
|
50
|
+
* lifecycle as `#configErrors`.
|
|
51
|
+
*/
|
|
52
|
+
#ruleDeprecations;
|
|
53
|
+
/**
|
|
54
|
+
* Rule-mapping errors for the CURRENT document. Reset (not accumulated) on
|
|
55
|
+
* every `#createDocument()` so repeated `setCode()` calls don't duplicate
|
|
56
|
+
* them. See https://github.com/markuplint/markuplint/issues/3900.
|
|
57
|
+
*/
|
|
58
|
+
#mappingErrors = [];
|
|
28
59
|
/**
|
|
29
60
|
* Pre-expansion nodeRules preserved for hot-reload.
|
|
30
61
|
* When `update()` is called without a new ruleset, these are used as the
|
|
@@ -33,12 +64,8 @@ export class MLCore {
|
|
|
33
64
|
*/
|
|
34
65
|
#originalNodeRules;
|
|
35
66
|
#originalChildNodeRules;
|
|
36
|
-
/**
|
|
37
|
-
* Pre-computed namespace prefixes from wildcard disable entries.
|
|
38
|
-
* e.g., `rules["a11y/*"]: false` yields `"a11y/"`.
|
|
39
|
-
*/
|
|
40
67
|
#disabledNamespaces;
|
|
41
|
-
constructor({ parser, sourceCode, ruleset, rules, locale, schemas, ruleCommonSettings, parserOptions, severity, pretenders, filename, debug, configErrors, }) {
|
|
68
|
+
constructor({ parser, sourceCode, ruleset, rules, locale, schemas, ruleCommonSettings, parserOptions, severity, pretenders, filename, debug, configErrors, ruleDeprecations, }) {
|
|
42
69
|
if (debug) {
|
|
43
70
|
enableDebug();
|
|
44
71
|
}
|
|
@@ -52,12 +79,11 @@ export class MLCore {
|
|
|
52
79
|
this.#severity = severity;
|
|
53
80
|
this.#pretenders = [...pretenders];
|
|
54
81
|
this.#configErrors = [...(configErrors ?? [])];
|
|
82
|
+
this.#ruleDeprecations = [...(ruleDeprecations ?? [])];
|
|
55
83
|
// Preserve pre-expansion nodeRules for hot-reload
|
|
56
84
|
this.#originalNodeRules = ruleset.nodeRules ?? [];
|
|
57
85
|
this.#originalChildNodeRules = ruleset.childNodeRules ?? [];
|
|
58
|
-
// Expand named rule groups in the rules section
|
|
59
86
|
const namedRulesResult = expandNamedRules(ruleset.rules ?? {}, rules);
|
|
60
|
-
// Expand named nodeRules into virtual rules (using expanded rules as base)
|
|
61
87
|
const allRulesForExpansion = [...rules, ...namedRulesResult.virtualRules];
|
|
62
88
|
const nodeRuleResult = expandNamedNodeRules(this.#originalNodeRules, allRulesForExpansion);
|
|
63
89
|
const childNodeRuleResult = expandNamedNodeRules(this.#originalChildNodeRules, allRulesForExpansion);
|
|
@@ -97,22 +123,21 @@ export class MLCore {
|
|
|
97
123
|
*
|
|
98
124
|
* @param fabric - Partial fabric with the properties to update
|
|
99
125
|
*/
|
|
100
|
-
update({ parser, ruleset, rules, locale, schemas, parserOptions, configErrors }) {
|
|
126
|
+
update({ parser, ruleset, rules, locale, schemas, parserOptions, pretenders, configErrors, ruleDeprecations, }) {
|
|
101
127
|
this.#parser = parser ?? this.#parser;
|
|
102
128
|
this.#locale = locale ?? this.#locale;
|
|
103
129
|
this.#schemas = schemas ?? this.#schemas;
|
|
130
|
+
this.#pretenders = pretenders ? [...pretenders] : this.#pretenders;
|
|
104
131
|
this.#configErrors = [...(configErrors ?? [])];
|
|
132
|
+
this.#ruleDeprecations = [...(ruleDeprecations ?? [])];
|
|
105
133
|
const baseRules = rules ? [...rules] : this.#rules.filter(r => !r.baseRuleId);
|
|
106
|
-
// Use pre-expansion originals as fallback when ruleset is not provided
|
|
107
134
|
const incomingNodeRules = ruleset?.nodeRules ?? this.#originalNodeRules;
|
|
108
135
|
const incomingChildNodeRules = ruleset?.childNodeRules ?? this.#originalChildNodeRules;
|
|
109
|
-
// Expand named rule groups in the rules section
|
|
110
136
|
const incomingRules = ruleset?.rules ?? this.#ruleset.rules;
|
|
111
137
|
const namedRulesResult = expandNamedRules(incomingRules, baseRules);
|
|
112
138
|
const allRulesForExpansion = [...baseRules, ...namedRulesResult.virtualRules];
|
|
113
139
|
const nodeRuleResult = expandNamedNodeRules(incomingNodeRules, allRulesForExpansion);
|
|
114
140
|
const childNodeRuleResult = expandNamedNodeRules(incomingChildNodeRules, allRulesForExpansion);
|
|
115
|
-
// Update originals if new data was provided
|
|
116
141
|
if (ruleset?.nodeRules) {
|
|
117
142
|
this.#originalNodeRules = ruleset.nodeRules;
|
|
118
143
|
}
|
|
@@ -151,20 +176,24 @@ export class MLCore {
|
|
|
151
176
|
log('verify: error %o', this.#document.message);
|
|
152
177
|
return { violations, fixedCode: fix ? this.#sourceCode : undefined };
|
|
153
178
|
}
|
|
179
|
+
this.#pushNonFatalParseErrors(violations);
|
|
154
180
|
const definedRuleName = new Set(this.#rules.map(rule => rule.name));
|
|
155
181
|
const setRuleNames = new Set([
|
|
156
182
|
...Object.keys(this.#ruleset.rules),
|
|
157
183
|
...this.#ruleset.nodeRules.flatMap(nodeRule => Object.keys(nodeRule.rules ?? {})),
|
|
158
184
|
...this.#ruleset.childNodeRules.flatMap(childNodeRule => Object.keys(childNodeRule.rules ?? {})),
|
|
159
185
|
]);
|
|
186
|
+
// Config-level violations are independent of the source code, so they are
|
|
187
|
+
// collected separately to also be included in `finalPassViolations`.
|
|
188
|
+
const configViolations = [];
|
|
160
189
|
for (const setRuleName of setRuleNames) {
|
|
161
190
|
// Skip wildcard patterns (e.g., "a11y/*") — they are namespace disable entries, not rule references
|
|
162
191
|
if (setRuleName.endsWith('/*')) {
|
|
163
192
|
continue;
|
|
164
193
|
}
|
|
165
194
|
if (!definedRuleName.has(setRuleName)) {
|
|
166
|
-
|
|
167
|
-
ruleId:
|
|
195
|
+
configViolations.push({
|
|
196
|
+
ruleId: CONFIG_ERROR_RULE_ID,
|
|
168
197
|
severity: 'warning',
|
|
169
198
|
message: `Rule not found: ${setRuleName}`,
|
|
170
199
|
col: 1,
|
|
@@ -173,9 +202,9 @@ export class MLCore {
|
|
|
173
202
|
});
|
|
174
203
|
}
|
|
175
204
|
}
|
|
176
|
-
for (const error of this.#configErrors) {
|
|
177
|
-
|
|
178
|
-
ruleId:
|
|
205
|
+
for (const error of [...this.#configErrors, ...this.#mappingErrors]) {
|
|
206
|
+
configViolations.push({
|
|
207
|
+
ruleId: CONFIG_ERROR_RULE_ID,
|
|
179
208
|
severity: 'warning',
|
|
180
209
|
message: error.message,
|
|
181
210
|
col: 1,
|
|
@@ -183,6 +212,20 @@ export class MLCore {
|
|
|
183
212
|
raw: '',
|
|
184
213
|
});
|
|
185
214
|
}
|
|
215
|
+
const deprecationSeverity = this.#resolveDeprecationSeverity();
|
|
216
|
+
if (deprecationSeverity != null) {
|
|
217
|
+
for (const { deprecatedName, replacedBy } of this.#ruleDeprecations) {
|
|
218
|
+
configViolations.push({
|
|
219
|
+
ruleId: RULE_DEPRECATION_RULE_ID,
|
|
220
|
+
severity: deprecationSeverity,
|
|
221
|
+
message: `Rule "${deprecatedName}" is deprecated and will be removed in v6. Use ${replacedBy.join(', ')} instead.`,
|
|
222
|
+
col: 1,
|
|
223
|
+
line: 1,
|
|
224
|
+
raw: '',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
violations.push(...configViolations);
|
|
186
229
|
const ruleViolations = await this.#runAllRules(fix);
|
|
187
230
|
violations.push(...ruleViolations);
|
|
188
231
|
if (resultLog.enabled) {
|
|
@@ -200,7 +243,6 @@ export class MLCore {
|
|
|
200
243
|
resultLog('Warning: %d', w);
|
|
201
244
|
resultLog('Info: %d', i);
|
|
202
245
|
}
|
|
203
|
-
// Apply fixes if enabled
|
|
204
246
|
let fixedCode;
|
|
205
247
|
let fixSummary;
|
|
206
248
|
if (fix) {
|
|
@@ -209,16 +251,28 @@ export class MLCore {
|
|
|
209
251
|
const originalSourceCode = this.#sourceCode;
|
|
210
252
|
const originalAst = this.#ast;
|
|
211
253
|
const originalDocument = this.#document;
|
|
254
|
+
const originalMappingErrors = this.#mappingErrors;
|
|
212
255
|
try {
|
|
213
256
|
const fixResult = await this.#multiPassFix(violations);
|
|
214
257
|
fixedCode = fixResult.code;
|
|
215
258
|
fixSummary = fixResult.summary;
|
|
259
|
+
if (configViolations.length > 0 && fixSummary.finalPassViolations) {
|
|
260
|
+
// Config-level violations persist regardless of fixing;
|
|
261
|
+
// keep them visible in the post-fix violation list.
|
|
262
|
+
fixSummary = {
|
|
263
|
+
...fixSummary,
|
|
264
|
+
finalPassViolations: [...configViolations, ...fixSummary.finalPassViolations],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
216
267
|
}
|
|
217
268
|
finally {
|
|
218
|
-
// Restore original state - verify() must be non-mutating
|
|
269
|
+
// Restore original state - verify() must be non-mutating.
|
|
270
|
+
// The fix loop re-parses, and each #createDocument resets
|
|
271
|
+
// #mappingErrors; restore it to the pre-fix document's.
|
|
219
272
|
this.#sourceCode = originalSourceCode;
|
|
220
273
|
this.#ast = originalAst;
|
|
221
274
|
this.#document = originalDocument;
|
|
275
|
+
this.#mappingErrors = originalMappingErrors;
|
|
222
276
|
}
|
|
223
277
|
}
|
|
224
278
|
else {
|
|
@@ -236,6 +290,10 @@ export class MLCore {
|
|
|
236
290
|
return { violations, fixedCode, fixSummary };
|
|
237
291
|
}
|
|
238
292
|
#createDocument() {
|
|
293
|
+
// Reset up front: mapping errors belong to the document being (re)built,
|
|
294
|
+
// so a failed parse or build must not leave a previous document's errors
|
|
295
|
+
// behind. Repopulated below only on a successful build. See #3900.
|
|
296
|
+
this.#mappingErrors = [];
|
|
239
297
|
if (!this.#ast) {
|
|
240
298
|
return;
|
|
241
299
|
}
|
|
@@ -247,8 +305,11 @@ export class MLCore {
|
|
|
247
305
|
tagNameCaseSensitive: this.#parser.tagNameCaseSensitive,
|
|
248
306
|
pretenders: this.#pretenders,
|
|
249
307
|
});
|
|
250
|
-
// Collect errors from rule mapping (e.g., invalid wildcard usage)
|
|
251
|
-
|
|
308
|
+
// Collect errors from rule mapping (e.g., invalid wildcard usage).
|
|
309
|
+
// Reset rather than append: the Document constructor regenerates
|
|
310
|
+
// `mappingErrors` on every call, so accumulating them would duplicate
|
|
311
|
+
// the same errors on each re-parse (e.g. via setCode). See #3900.
|
|
312
|
+
this.#mappingErrors = [...this.#ruleset.mappingErrors];
|
|
252
313
|
this.#ruleset.mappingErrors.length = 0;
|
|
253
314
|
}
|
|
254
315
|
catch (error) {
|
|
@@ -260,14 +321,126 @@ export class MLCore {
|
|
|
260
321
|
}
|
|
261
322
|
}
|
|
262
323
|
}
|
|
263
|
-
|
|
264
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Surfaces non-fatal parser conformance errors collected by the underlying
|
|
326
|
+
* parser (e.g., parse5's `onParseError` events on `MLASTDocument.parseErrors`)
|
|
327
|
+
* via the same `parse-error` violation channel as fatal `ParserError`s.
|
|
328
|
+
*
|
|
329
|
+
* Order contract (relied on by every rule spec under
|
|
330
|
+
* `@markuplint/rules/src/**`): each `parseErrors` entry is pushed in the
|
|
331
|
+
* order the parser emitted it, *before* any rule iteration runs. Tests
|
|
332
|
+
* that match on `toStrictEqual([...])` depend on this position.
|
|
333
|
+
*
|
|
334
|
+
* Severity resolution:
|
|
335
|
+
*
|
|
336
|
+
* - `severity.parseError` is a `Partial<Record<MLASTParseErrorCode, …>>` →
|
|
337
|
+
* each entry's `code` looks up its own severity; codes absent from the
|
|
338
|
+
* record default to `'off'` (suppressed).
|
|
339
|
+
* - `severity.parseError` is a single severity string/boolean → applied
|
|
340
|
+
* uniformly to every entry.
|
|
341
|
+
* - `severity.parseError` is unset → **all non-fatal codes are off**.
|
|
342
|
+
* Users must opt in explicitly.
|
|
343
|
+
*
|
|
344
|
+
* @param violations The verify-time violations array to mutate.
|
|
345
|
+
*/
|
|
346
|
+
#pushNonFatalParseErrors(violations) {
|
|
347
|
+
const parseErrors = this.#ast?.parseErrors;
|
|
348
|
+
if (!parseErrors) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
// Build the Set of parse5 codes that an active ml rule has claimed
|
|
352
|
+
// responsibility for via `meta.mirrorsParseErrorCodes`. The user's
|
|
353
|
+
// ruleset decides whether each rule's declaration is in scope:
|
|
354
|
+
//
|
|
355
|
+
// - Rule **set** in the ruleset (config !== undefined) — whatever the
|
|
356
|
+
// value (`true`, `false`, severity, object) — the user has expressed
|
|
357
|
+
// intent about this check. Honour the mirror declaration:
|
|
358
|
+
// - active: the rule itself will report violations for those codes
|
|
359
|
+
// - disabled (`false`): the user explicitly opted out, so the channel
|
|
360
|
+
// stays silent too — no surprise re-surfacing
|
|
361
|
+
// - Rule **not mentioned** (config === undefined) — pure default. The
|
|
362
|
+
// parse-error channel remains the channel of record for those codes
|
|
363
|
+
// and surfaces them when the user has opted in via `severity.parseError`.
|
|
364
|
+
//
|
|
365
|
+
// This keeps the responsibility clean: rule packages declare what they
|
|
366
|
+
// cover (static metadata); ml-core honours the user's ruleset choice;
|
|
367
|
+
// no per-node logic, no hard-coded code→rule map.
|
|
368
|
+
const mirroredCodes = new Set();
|
|
369
|
+
for (const rule of this.#rules) {
|
|
370
|
+
if (rule.mirrorsParseErrorCodes.length === 0) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
// The rule is "mentioned in the ruleset" if **either** the alias
|
|
374
|
+
// name OR the base rule name has an entry. Two entry styles exist:
|
|
375
|
+
//
|
|
376
|
+
// - Direct user configs use base rule names: `rules.attr-duplication`
|
|
377
|
+
// - Preset named nodeRules use alias names: `rules['html-standard/attr-duplication']`
|
|
378
|
+
//
|
|
379
|
+
// `MLRule` for a preset-aliased entry has `rule.name = 'html-standard/...'`
|
|
380
|
+
// and `rule.baseRuleId = 'attr-duplication'`; for a direct entry,
|
|
381
|
+
// `rule.name = 'attr-duplication'` and `baseRuleId` is undefined.
|
|
382
|
+
// Checking both names covers both styles.
|
|
383
|
+
const aliasConfig = this.#ruleset.rules[rule.name];
|
|
384
|
+
const baseConfig = rule.baseRuleId === undefined ? undefined : this.#ruleset.rules[rule.baseRuleId];
|
|
385
|
+
if (aliasConfig === undefined && baseConfig === undefined) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
for (const code of rule.mirrorsParseErrorCodes) {
|
|
389
|
+
mirroredCodes.add(code);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const parserError of parseErrors) {
|
|
393
|
+
if (mirroredCodes.has(parserError.code)) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const violation = this.#createParseError(`Parser conformance error: ${parserError.code}`, parserError.startLine, parserError.startCol, parserError.raw, parserError.code);
|
|
397
|
+
if (violation) {
|
|
398
|
+
violations.push(violation);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Resolves `severity.deprecation`, honouring its single-value form only
|
|
404
|
+
* (unlike `severity.parseError`, there's no fixed enum of deprecated rule
|
|
405
|
+
* names to key a per-code `Record` on).
|
|
406
|
+
*
|
|
407
|
+
* Unlike `#createParseError`'s parse-error channel, this defaults to
|
|
408
|
+
* `'warning'` (not off/suppressed) when unset — the deprecation channel
|
|
409
|
+
* is being carved out of the always-on `config-error` channel, not
|
|
410
|
+
* introduced cold, so leaving the option unset must not silence a notice
|
|
411
|
+
* users already see today.
|
|
412
|
+
*
|
|
413
|
+
* @returns the resolved severity, or `null` if suppressed.
|
|
414
|
+
*/
|
|
415
|
+
#resolveDeprecationSeverity() {
|
|
416
|
+
return resolveUniformSeverity(this.#severity.deprecation, 'warning');
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Builds a `ruleId: 'parse-error'` violation, honouring
|
|
420
|
+
* `severity.parseError`.
|
|
421
|
+
*
|
|
422
|
+
* If `code` is supplied (non-fatal `parseErrors` entry) and the option is
|
|
423
|
+
* a `Partial<Record<code, severity>>`, the per-code value is used (absent
|
|
424
|
+
* codes default to `'off'`). If the option is unset, non-fatal entries
|
|
425
|
+
* are suppressed (opt-in only) while fatal errors (no `code`) still
|
|
426
|
+
* default to `'error'`.
|
|
427
|
+
*
|
|
428
|
+
* @returns the violation, or `null` if suppressed.
|
|
429
|
+
*/
|
|
430
|
+
#createParseError(message, line, col, raw, code) {
|
|
431
|
+
const cfg = this.#severity.parseError;
|
|
432
|
+
// Fatal ParserErrors (no `code`) can't be targeted by the per-code
|
|
433
|
+
// Record form, so they fall back to `'error'` there; under the
|
|
434
|
+
// uniform form they default to `'error'` too, while non-fatal entries
|
|
435
|
+
// default to suppressed when unset — see `resolveUniformSeverity`.
|
|
436
|
+
const severity = typeof cfg === 'object'
|
|
437
|
+
? code == null
|
|
438
|
+
? 'error'
|
|
439
|
+
: resolveUniformSeverity(cfg[code], null)
|
|
440
|
+
: resolveUniformSeverity(cfg, code == null ? 'error' : null);
|
|
441
|
+
if (severity == null) {
|
|
265
442
|
return null;
|
|
266
443
|
}
|
|
267
|
-
// Default severity is 'error'
|
|
268
|
-
const severity = this.#severity.parseError === true || this.#severity.parseError == null
|
|
269
|
-
? 'error'
|
|
270
|
-
: this.#severity.parseError;
|
|
271
444
|
return {
|
|
272
445
|
ruleId: 'parse-error',
|
|
273
446
|
severity,
|
|
@@ -278,23 +451,29 @@ export class MLCore {
|
|
|
278
451
|
};
|
|
279
452
|
}
|
|
280
453
|
/**
|
|
281
|
-
*
|
|
282
|
-
* fixes remain or the maximum pass count is reached (ESLint-style multi-pass loop).
|
|
454
|
+
* ESLint-style multi-pass fix loop (modeled after SourceCodeFixer).
|
|
283
455
|
*
|
|
284
456
|
* **Callers must save/restore `#sourceCode`, `#ast`, and `#document`** because
|
|
285
457
|
* this method mutates them during intermediate re-parse steps.
|
|
286
|
-
*
|
|
287
|
-
* @param initialViolations - Violations from the first verification pass
|
|
288
|
-
* @returns The final fixed source code and a summary of the fix process
|
|
289
458
|
*/
|
|
290
459
|
async #multiPassFix(initialViolations) {
|
|
460
|
+
// Same safety cap as ESLint's SourceCodeFixer (10 passes).
|
|
291
461
|
const MAX_FIX_PASSES = 10;
|
|
292
462
|
let currentCode = this.#sourceCode;
|
|
293
|
-
let previousCode;
|
|
294
463
|
let fixes = extractFixes(initialViolations);
|
|
295
464
|
let totalApplied = 0;
|
|
296
465
|
let totalSkipped = 0;
|
|
297
466
|
let firstPassEdits = [];
|
|
467
|
+
// The input code of each pass, keyed by code string, for N-pass cycle
|
|
468
|
+
// detection (A → B → A as well as longer cycles such as A → B → C → A).
|
|
469
|
+
const codeHistory = new Map([[currentCode, 0]]);
|
|
470
|
+
// Violations from the latest #runAllRules call, valid for `currentCode`.
|
|
471
|
+
// Reused by the final verification below to avoid a redundant re-run
|
|
472
|
+
// when the loop already re-verified the final code.
|
|
473
|
+
let latestViolations;
|
|
474
|
+
// The most recent code that is known to parse successfully. Used to roll
|
|
475
|
+
// back a pass whose output fails to parse.
|
|
476
|
+
let lastParsableCode = currentCode;
|
|
298
477
|
let pass = 0;
|
|
299
478
|
for (; pass < MAX_FIX_PASSES; pass++) {
|
|
300
479
|
log('fix pass %d: %d fixes', pass, fixes.length);
|
|
@@ -312,31 +491,42 @@ export class MLCore {
|
|
|
312
491
|
log('fix pass %d: output unchanged, stopping', pass);
|
|
313
492
|
break;
|
|
314
493
|
}
|
|
315
|
-
// Cycle detection: if the output matches the
|
|
316
|
-
// fixes are oscillating (A → B → A) and will never converge.
|
|
317
|
-
|
|
318
|
-
|
|
494
|
+
// Cycle detection: if the output matches the input of any earlier pass,
|
|
495
|
+
// fixes are oscillating (A → B → A, A → B → C → A, ...) and will never converge.
|
|
496
|
+
const cycleStart = codeHistory.get(result.output);
|
|
497
|
+
if (cycleStart !== undefined) {
|
|
498
|
+
log('fix pass %d: cycle detected (output matches the input of pass %d, cycle length %d), stopping', pass, cycleStart, pass + 1 - cycleStart);
|
|
319
499
|
currentCode = result.output;
|
|
500
|
+
latestViolations = undefined;
|
|
320
501
|
break;
|
|
321
502
|
}
|
|
322
|
-
previousCode = currentCode;
|
|
323
503
|
currentCode = result.output;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
//
|
|
329
|
-
log('fix pass %d: %d skipped, re-parsing for next pass', pass, result.skipped.length);
|
|
330
|
-
const previousGoodCode = currentCode;
|
|
504
|
+
codeHistory.set(currentCode, pass + 1);
|
|
505
|
+
latestViolations = undefined;
|
|
506
|
+
// Parse the output immediately — for the next pass or for the final
|
|
507
|
+
// verification — so an unparsable output is always rolled back
|
|
508
|
+
// instead of being returned (and written to disk) broken.
|
|
331
509
|
this.#sourceCode = currentCode;
|
|
332
510
|
this.#parse();
|
|
333
511
|
this.#createDocument();
|
|
334
512
|
if (this.#document instanceof ParserError) {
|
|
335
|
-
log('fix pass %d: produced unparsable code, reverting to previous
|
|
336
|
-
currentCode =
|
|
513
|
+
log('fix pass %d: produced unparsable code, reverting to the previous parsable code', pass);
|
|
514
|
+
currentCode = lastParsableCode;
|
|
515
|
+
totalApplied -= result.applied.length;
|
|
516
|
+
if (pass === 0) {
|
|
517
|
+
firstPassEdits = [];
|
|
518
|
+
}
|
|
337
519
|
break;
|
|
338
520
|
}
|
|
521
|
+
lastParsableCode = currentCode;
|
|
522
|
+
if (result.skipped.length === 0) {
|
|
523
|
+
log('fix pass %d: all fixes applied, stopping', pass);
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
// --- Multi-pass path (only when overlapping fixes exist) ---
|
|
527
|
+
log('fix pass %d: %d skipped, re-running rules for next pass', pass, result.skipped.length);
|
|
339
528
|
const newViolations = await this.#runAllRules(true);
|
|
529
|
+
latestViolations = newViolations;
|
|
340
530
|
fixes = extractFixes(newViolations);
|
|
341
531
|
if (fixes.length === 0) {
|
|
342
532
|
log('fix pass %d: no more fixable violations, stopping', pass);
|
|
@@ -347,6 +537,29 @@ export class MLCore {
|
|
|
347
537
|
if (reachedMaxPasses) {
|
|
348
538
|
log('fix: reached maximum number of passes (%d), some fixes may not have been applied', MAX_FIX_PASSES);
|
|
349
539
|
}
|
|
540
|
+
// Final verification (#3890): compute the violations that remain in the
|
|
541
|
+
// final code. When the loop already re-verified `currentCode`, reuse that
|
|
542
|
+
// result; otherwise re-run rules once (re-parsing first unless the parsed
|
|
543
|
+
// state already corresponds to `currentCode`).
|
|
544
|
+
let finalPassViolations;
|
|
545
|
+
if (totalApplied > 0) {
|
|
546
|
+
if (latestViolations === undefined) {
|
|
547
|
+
if (this.#sourceCode !== currentCode) {
|
|
548
|
+
this.#sourceCode = currentCode;
|
|
549
|
+
this.#parse();
|
|
550
|
+
this.#createDocument();
|
|
551
|
+
}
|
|
552
|
+
if (!(this.#document instanceof ParserError)) {
|
|
553
|
+
latestViolations = await this.#runAllRules(true);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (latestViolations !== undefined) {
|
|
557
|
+
const collected = [];
|
|
558
|
+
this.#pushNonFatalParseErrors(collected);
|
|
559
|
+
collected.push(...latestViolations);
|
|
560
|
+
finalPassViolations = collected;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
350
563
|
return {
|
|
351
564
|
code: currentCode,
|
|
352
565
|
summary: {
|
|
@@ -355,16 +568,10 @@ export class MLCore {
|
|
|
355
568
|
totalSkipped,
|
|
356
569
|
reachedMaxPasses,
|
|
357
570
|
firstPassEdits,
|
|
571
|
+
finalPassViolations,
|
|
358
572
|
},
|
|
359
573
|
};
|
|
360
574
|
}
|
|
361
|
-
/**
|
|
362
|
-
* Executes all configured rules against the current document and collects violations.
|
|
363
|
-
* Skips disabled rules and handles virtual rule disable conditions.
|
|
364
|
-
*
|
|
365
|
-
* @param fix - Whether to execute fix callbacks on violations
|
|
366
|
-
* @returns All violations produced by the rule set
|
|
367
|
-
*/
|
|
368
575
|
async #runAllRules(fix) {
|
|
369
576
|
const violations = [];
|
|
370
577
|
if (this.#document instanceof ParserError) {
|
|
@@ -425,16 +632,29 @@ export class MLCore {
|
|
|
425
632
|
}
|
|
426
633
|
}
|
|
427
634
|
/**
|
|
428
|
-
*
|
|
429
|
-
*
|
|
635
|
+
* Normalizes a single-value `Severity | 'off' | boolean | undefined` option
|
|
636
|
+
* (the shape `severity.deprecation` and `severity.parseError`'s uniform/
|
|
637
|
+
* per-code leaf values share) to a resolved severity or `null` (suppressed).
|
|
638
|
+
*
|
|
639
|
+
* Shared so the same three-way rule — `false`/`'off'` → suppressed, `true` →
|
|
640
|
+
* `'error'`, unset → `defaultWhenUnset` — isn't reimplemented at each call
|
|
641
|
+
* site as channels using this shape are added.
|
|
430
642
|
*/
|
|
643
|
+
function resolveUniformSeverity(cfg, defaultWhenUnset) {
|
|
644
|
+
if (cfg === false || cfg === 'off') {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
if (cfg == null) {
|
|
648
|
+
return defaultWhenUnset;
|
|
649
|
+
}
|
|
650
|
+
return cfg === true ? 'error' : cfg;
|
|
651
|
+
}
|
|
431
652
|
function extractDisabledNamespaces(rules) {
|
|
432
653
|
return Object.entries(rules)
|
|
433
654
|
.filter(([key, value]) => key.endsWith('/*') && value === false)
|
|
434
655
|
.map(([key]) => key.slice(0, -1)); // "a11y/*" → "a11y/"
|
|
435
656
|
}
|
|
436
657
|
/**
|
|
437
|
-
* Builds a mapping from base rule names to virtual rule names.
|
|
438
658
|
* Used by nodeRules/childNodeRules to propagate settings (especially `false`)
|
|
439
659
|
* to virtual rules created by NamedRuleGroups.
|
|
440
660
|
*/
|
|
@@ -456,12 +676,6 @@ virtualRules) {
|
|
|
456
676
|
}
|
|
457
677
|
return map;
|
|
458
678
|
}
|
|
459
|
-
/**
|
|
460
|
-
* Collects all `FixData` from violations that have a fix callback result.
|
|
461
|
-
*
|
|
462
|
-
* @param violations - The violations to extract fixes from
|
|
463
|
-
* @returns An array of `FixData` objects ready for `applyFixes()`
|
|
464
|
-
*/
|
|
465
679
|
function extractFixes(violations) {
|
|
466
680
|
const fixes = [];
|
|
467
681
|
for (const v of violations) {
|
|
@@ -1,11 +1,3 @@
|
|
|
1
1
|
import type { MLElement } from '../node/element.js';
|
|
2
2
|
import type { ARIAVersion } from '@markuplint/ml-spec';
|
|
3
|
-
/**
|
|
4
|
-
* Computes the accessible name for an MLElement using the HTML-AAM algorithm.
|
|
5
|
-
* Creates an MLCore-specific resolver that bridges MLElement to the AccnameResolver interface.
|
|
6
|
-
*
|
|
7
|
-
* @param el - The MLElement to compute the accessible name for
|
|
8
|
-
* @param version - The ARIA specification version to use for role resolution
|
|
9
|
-
* @returns The computed accessible name string, or an empty string on error
|
|
10
|
-
*/
|
|
11
3
|
export declare function getAccname(el: MLElement<any, any>, version: ARIAVersion): string;
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import { computeAccessibleName, escapeCSS, getComputedRole, EMBEDDED_CONTROL_ROLES, isNativeEmbeddedControl, } from '@markuplint/ml-spec';
|
|
2
2
|
import { log } from '../../debug.js';
|
|
3
3
|
const accnameLog = log.extend('accname');
|
|
4
|
-
/**
|
|
5
|
-
* Computes the accessible name for an MLElement using the HTML-AAM algorithm.
|
|
6
|
-
* Creates an MLCore-specific resolver that bridges MLElement to the AccnameResolver interface.
|
|
7
|
-
*
|
|
8
|
-
* @param el - The MLElement to compute the accessible name for
|
|
9
|
-
* @param version - The ARIA specification version to use for role resolution
|
|
10
|
-
* @returns The computed accessible name string, or an empty string on error
|
|
11
|
-
*/
|
|
12
4
|
export function getAccname(
|
|
13
5
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
14
6
|
el, version) {
|
|
@@ -18,8 +10,13 @@ el, version) {
|
|
|
18
10
|
// environment differences (e.g., Deno lacking certain DOM APIs).
|
|
19
11
|
// A single element's failure must not abort the entire linting process,
|
|
20
12
|
// so we catch all errors and return an empty name (= "unnamed").
|
|
21
|
-
// This is an intentional exception to the Tier 1 re-throw policy
|
|
22
|
-
//
|
|
13
|
+
// This is an intentional exception to the Tier 1 re-throw policy (see
|
|
14
|
+
// `isFatalError()` in `@markuplint/shared`): Tier-1-shaped errors here
|
|
15
|
+
// can be caused by the runtime environment, not only implementation bugs.
|
|
16
|
+
// The empty-string result is not converted to a violation either — a
|
|
17
|
+
// single element's accname failure must not create noise across
|
|
18
|
+
// unrelated rules, and "unnamed" still drives meaningful a11y rule
|
|
19
|
+
// violations downstream.
|
|
23
20
|
try {
|
|
24
21
|
const resolver = createMLCoreResolver(el, version);
|
|
25
22
|
const result = computeAccessibleName(el, resolver);
|
package/lib/ml-dom/node/attr.js
CHANGED
|
@@ -131,7 +131,9 @@ export class MLAttr extends MLNode {
|
|
|
131
131
|
this.isDirective = this._astToken.isDirective;
|
|
132
132
|
this.isDuplicatable = this._astToken.isDuplicatable;
|
|
133
133
|
}
|
|
134
|
-
// IDL attribute resolution (after directivePatterns)
|
|
134
|
+
// IDL attribute resolution (after directivePatterns).
|
|
135
|
+
// Performed in the core, not in each parser: any spec opting in via
|
|
136
|
+
// `acceptedAttrNames` shares the same IDL-to-content-attribute mapping.
|
|
135
137
|
if (ownElement.ownerMLDocument.specs.acceptedAttrNames && !this.isDirective) {
|
|
136
138
|
const { contentAttrName, idlPropName } = searchIDLAttribute(this.#potentialName);
|
|
137
139
|
if (contentAttrName && contentAttrName !== this.#potentialName) {
|
|
@@ -9,6 +9,12 @@ import { MLNode } from './node.js';
|
|
|
9
9
|
* These nodes correspond to template engine constructs such as conditionals (`if`/`else`),
|
|
10
10
|
* loops (`each`), and other preprocessor directives that are not part of standard HTML.
|
|
11
11
|
*
|
|
12
|
+
* Serves as the bridge between template syntax and HTML content model
|
|
13
|
+
* validation: transparency keeps the wrapper invisible to DOM traversal so
|
|
14
|
+
* rules such as `permitted-contents` see the effective HTML children, while
|
|
15
|
+
* `blockBehavior` lets `conditionalChildNodes()` enumerate every possible
|
|
16
|
+
* rendering branch.
|
|
17
|
+
*
|
|
12
18
|
* @template T - The rule configuration value type
|
|
13
19
|
* @template O - The rule options type
|
|
14
20
|
*/
|
package/lib/ml-dom/node/block.js
CHANGED
|
@@ -5,6 +5,12 @@ import { MLNode } from './node.js';
|
|
|
5
5
|
* These nodes correspond to template engine constructs such as conditionals (`if`/`else`),
|
|
6
6
|
* loops (`each`), and other preprocessor directives that are not part of standard HTML.
|
|
7
7
|
*
|
|
8
|
+
* Serves as the bridge between template syntax and HTML content model
|
|
9
|
+
* validation: transparency keeps the wrapper invisible to DOM traversal so
|
|
10
|
+
* rules such as `permitted-contents` see the effective HTML children, while
|
|
11
|
+
* `blockBehavior` lets `conditionalChildNodes()` enumerate every possible
|
|
12
|
+
* rendering branch.
|
|
13
|
+
*
|
|
8
14
|
* @template T - The rule configuration value type
|
|
9
15
|
* @template O - The rule options type
|
|
10
16
|
*/
|
|
@@ -15,13 +15,4 @@ import type { PlainData, RuleConfigValue } from '@markuplint/ml-config';
|
|
|
15
15
|
* @see https://dom.spec.whatwg.org/#idl-index
|
|
16
16
|
*/
|
|
17
17
|
export type MLChildNode<T extends RuleConfigValue, O extends PlainData = undefined> = MLDocumentType<T, O> | MLCharacterData<T, O> | MLElement<T, O> | MLBlock<T, O>;
|
|
18
|
-
/**
|
|
19
|
-
* Determines whether the given node is a child node type
|
|
20
|
-
* (DocumentType, CDATA, Comment, Text, Element, or preprocessor block).
|
|
21
|
-
*
|
|
22
|
-
* @template T - The rule configuration value type
|
|
23
|
-
* @template O - The rule options type
|
|
24
|
-
* @param node - The node to check
|
|
25
|
-
* @returns True if the node is one of the child node types
|
|
26
|
-
*/
|
|
27
18
|
export declare function isChildNode<T extends RuleConfigValue, O extends PlainData = undefined>(node: MLNode<T, O>): node is MLChildNode<T, O>;
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Determines whether the given node is a child node type
|
|
3
|
-
* (DocumentType, CDATA, Comment, Text, Element, or preprocessor block).
|
|
4
|
-
*
|
|
5
|
-
* @template T - The rule configuration value type
|
|
6
|
-
* @template O - The rule options type
|
|
7
|
-
* @param node - The node to check
|
|
8
|
-
* @returns True if the node is one of the child node types
|
|
9
|
-
*/
|
|
10
1
|
export function isChildNode(
|
|
11
2
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
12
3
|
node) {
|