@markuplint/ml-core 5.0.0-rc.6 → 5.0.0-rc.7

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 CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.0.0-rc.7](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.6...v5.0.0-rc.7) (2026-08-31)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **ml-core:** fix named rule group disable propagation and validation ([139f466](https://github.com/markuplint/markuplint/commit/139f4661a12ac10b15ade305c30bbd8234b07b0f))
11
+
6
12
  # [5.0.0-rc.6](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.5...v5.0.0-rc.6) (2026-08-30)
7
13
 
8
14
  ### Features
package/lib/ml-core.js CHANGED
@@ -94,6 +94,11 @@ export class MLCore {
94
94
  nodeRules: nodeRuleResult.transformedNodeRules,
95
95
  childNodeRules: childNodeRuleResult.transformedNodeRules,
96
96
  baseRuleToVirtualNames: buildBaseRuleToVirtualNames(namedRulesResult.virtualRules),
97
+ baseRuleToScopedVirtualNames: buildBaseRuleToVirtualNames([
98
+ ...nodeRuleResult.virtualRules,
99
+ ...childNodeRuleResult.virtualRules,
100
+ ]),
101
+ knownNamedRuleGroupKeys: ruleset.knownNamedRuleGroupKeys ?? new Set(),
97
102
  mappingErrors: [],
98
103
  };
99
104
  this.#disabledNamespaces = extractDisabledNamespaces(resolvedRules);
@@ -151,6 +156,11 @@ export class MLCore {
151
156
  nodeRules: nodeRuleResult.transformedNodeRules,
152
157
  childNodeRules: childNodeRuleResult.transformedNodeRules,
153
158
  baseRuleToVirtualNames: buildBaseRuleToVirtualNames(namedRulesResult.virtualRules),
159
+ baseRuleToScopedVirtualNames: buildBaseRuleToVirtualNames([
160
+ ...nodeRuleResult.virtualRules,
161
+ ...childNodeRuleResult.virtualRules,
162
+ ]),
163
+ knownNamedRuleGroupKeys: ruleset?.knownNamedRuleGroupKeys ?? this.#ruleset.knownNamedRuleGroupKeys,
154
164
  mappingErrors: [],
155
165
  };
156
166
  this.#disabledNamespaces = extractDisabledNamespaces(resolvedRules);
@@ -191,6 +201,15 @@ export class MLCore {
191
201
  if (setRuleName.endsWith('/*')) {
192
202
  continue;
193
203
  }
204
+ // Skip a named rule group disabled via top-level `rules` (e.g. "html-standard/foo": false).
205
+ // `expandNamedRules` intentionally doesn't instantiate a virtual rule when the merged value
206
+ // already resolved to `false` (there's nothing to run), so it never appears in `definedRuleName`.
207
+ // `knownNamedRuleGroupKeys` (threaded from config merge — see its JSDoc) tells a genuine
208
+ // named-group disable apart from a typo'd/nonexistent rule name that also happens to be
209
+ // set to `false` with a `/` in its key; only the former is exempt from this check.
210
+ if (this.#ruleset.rules[setRuleName] === false && this.#ruleset.knownNamedRuleGroupKeys.has(setRuleName)) {
211
+ continue;
212
+ }
194
213
  if (!definedRuleName.has(setRuleName)) {
195
214
  configViolations.push({
196
215
  ruleId: CONFIG_ERROR_RULE_ID,
@@ -2440,7 +2440,9 @@ export class MLDocument extends MLParentNode {
2440
2440
  specificity: matches.specificity,
2441
2441
  rule: mergedRule,
2442
2442
  });
2443
- // Propagate to virtual rules that wrap this base rule
2443
+ // Propagate to top-level-group virtual rules that wrap this base rule. These
2444
+ // wrappers have no selector scope of their own (they mirror the base rule
2445
+ // globally), so any matched value is safe to forward.
2444
2446
  const virtualNames = ruleset.baseRuleToVirtualNames.get(ruleName);
2445
2447
  if (virtualNames) {
2446
2448
  for (const vName of virtualNames) {
@@ -2453,6 +2455,26 @@ export class MLDocument extends MLParentNode {
2453
2455
  });
2454
2456
  }
2455
2457
  }
2458
+ // Propagate a disable to selector-scoped (nodeRules/childNodeRules-named) virtual
2459
+ // rules that wrap this base rule. Limited to `false`: unlike the top-level-group
2460
+ // case above, these wrappers DO have their own selector scope, so forwarding a
2461
+ // non-false value would apply their semantics (e.g. a required attribute name) to
2462
+ // every node this (unrelated) nodeRule matches, even where the virtual rule's own
2463
+ // selector never matched — see issue #4023's fix history for the regression this caused.
2464
+ if (convertedRule === false) {
2465
+ const scopedVirtualNames = ruleset.baseRuleToScopedVirtualNames.get(ruleName);
2466
+ if (scopedVirtualNames) {
2467
+ for (const vName of scopedVirtualNames) {
2468
+ // No merge needed: mergeRule(_, false) always returns false, so the
2469
+ // disable is unconditional regardless of any global config for vName.
2470
+ ruleMapper.set(node, vName, {
2471
+ from: 'nodeRules',
2472
+ specificity: matches.specificity,
2473
+ rule: false,
2474
+ });
2475
+ }
2476
+ }
2477
+ }
2456
2478
  }
2457
2479
  }
2458
2480
  // overwrite rule to child node
@@ -2518,7 +2540,8 @@ export class MLDocument extends MLParentNode {
2518
2540
  rule: mergedRule,
2519
2541
  });
2520
2542
  }
2521
- // Propagate to virtual rules that wrap this base rule
2543
+ // Propagate to top-level-group virtual rules see the matching nodeRules
2544
+ // propagation above for why any value is safe to forward here.
2522
2545
  const virtualNames = ruleset.baseRuleToVirtualNames.get(ruleName);
2523
2546
  if (virtualNames) {
2524
2547
  for (const vName of virtualNames) {
@@ -2533,6 +2556,25 @@ export class MLDocument extends MLParentNode {
2533
2556
  }
2534
2557
  }
2535
2558
  }
2559
+ // Propagate a disable to selector-scoped virtual rules. Limited to `false` —
2560
+ // see the matching nodeRules propagation above for why a non-false value must
2561
+ // not be forwarded to a virtual rule whose own selector didn't match.
2562
+ if (convertedRule === false) {
2563
+ const scopedVirtualNames = ruleset.baseRuleToScopedVirtualNames.get(ruleName);
2564
+ if (scopedVirtualNames) {
2565
+ for (const vName of scopedVirtualNames) {
2566
+ // No merge needed: mergeRule(_, false) always returns false, so the
2567
+ // disable is unconditional regardless of any global config for vName.
2568
+ for (const descendant of targetDescendants) {
2569
+ ruleMapper.set(descendant, vName, {
2570
+ from: 'childNodeRules',
2571
+ specificity: matches.specificity,
2572
+ rule: false,
2573
+ });
2574
+ }
2575
+ }
2576
+ }
2577
+ }
2536
2578
  }
2537
2579
  }
2538
2580
  }
@@ -5,14 +5,31 @@ import type { ChildNodeRule, Config, NodeRule, Rules } from '@markuplint/ml-conf
5
5
  */
6
6
  export declare class Ruleset {
7
7
  /**
8
- * Maps base rule names to their virtual rule names created by NamedRuleGroups.
9
- * For example, if `a11y/landmark-roles` wraps `no-nested-top-level-landmark`, this maps
10
- * `"no-nested-top-level-landmark"` `["a11y/landmark-roles"]`.
11
- * Used by nodeRules/childNodeRules to propagate settings to virtual rules.
8
+ * Maps base rule names to the virtual rule names created by top-level `rules`
9
+ * NamedRuleGroups (e.g. `"a11y/landmark-roles": { rules: { "no-nested-top-level-landmark": true } }`).
10
+ * These wrappers have no selector scope of their own — they mirror the base rule
11
+ * globally so nodeRules/childNodeRules may propagate *any* matched value to them,
12
+ * not just a disable.
12
13
  */
13
14
  readonly baseRuleToVirtualNames: ReadonlyMap<string, readonly string[]>;
15
+ /**
16
+ * Maps base rule names to the virtual rule names created by `nodeRules[].name` /
17
+ * `childNodeRules[].name` (selector-scoped named groups), as opposed to
18
+ * {@link baseRuleToVirtualNames}'s top-level `rules` groups. Because these wrappers
19
+ * DO have their own selector scope, only a disable (`false`) may be propagated to
20
+ * them from an unrelated nodeRules/childNodeRules entry — forwarding a non-false
21
+ * value would apply the wrapper's semantics to nodes its own selector never matched
22
+ * (see issue #4023's fix history).
23
+ */
24
+ readonly baseRuleToScopedVirtualNames: ReadonlyMap<string, readonly string[]>;
14
25
  /** Rule overrides that apply to child nodes matching specific selectors */
15
26
  readonly childNodeRules: readonly ChildNodeRule[];
27
+ /**
28
+ * `rules` keys that are a genuine NamedRuleGroup in some layer of the
29
+ * `extends`/override chain, carried over from {@link Config.knownNamedRuleGroupKeys}
30
+ * even where the final merged value collapsed to `false`. See that field's JSDoc.
31
+ */
32
+ readonly knownNamedRuleGroupKeys: ReadonlySet<string>;
16
33
  /**
17
34
  * Errors collected during rule mapping (e.g., invalid wildcard usage).
18
35
  * Consumed by MLCore and reported as config-error violations.
@@ -24,7 +41,8 @@ export declare class Ruleset {
24
41
  readonly rules: Rules;
25
42
  /**
26
43
  * @param config - The markuplint configuration to extract rules from
27
- * @param baseRuleToVirtualNames - Mapping from base rule names to virtual rule names
44
+ * @param baseRuleToVirtualNames - Mapping from base rule names to top-level-group virtual rule names
45
+ * @param baseRuleToScopedVirtualNames - Mapping from base rule names to selector-scoped (nodeRules/childNodeRules) virtual rule names
28
46
  */
29
- constructor(config: Config, baseRuleToVirtualNames?: ReadonlyMap<string, readonly string[]>);
47
+ constructor(config: Config, baseRuleToVirtualNames?: ReadonlyMap<string, readonly string[]>, baseRuleToScopedVirtualNames?: ReadonlyMap<string, readonly string[]>);
30
48
  }
@@ -4,14 +4,31 @@
4
4
  */
5
5
  export class Ruleset {
6
6
  /**
7
- * Maps base rule names to their virtual rule names created by NamedRuleGroups.
8
- * For example, if `a11y/landmark-roles` wraps `no-nested-top-level-landmark`, this maps
9
- * `"no-nested-top-level-landmark"` `["a11y/landmark-roles"]`.
10
- * Used by nodeRules/childNodeRules to propagate settings to virtual rules.
7
+ * Maps base rule names to the virtual rule names created by top-level `rules`
8
+ * NamedRuleGroups (e.g. `"a11y/landmark-roles": { rules: { "no-nested-top-level-landmark": true } }`).
9
+ * These wrappers have no selector scope of their own — they mirror the base rule
10
+ * globally so nodeRules/childNodeRules may propagate *any* matched value to them,
11
+ * not just a disable.
11
12
  */
12
13
  baseRuleToVirtualNames;
14
+ /**
15
+ * Maps base rule names to the virtual rule names created by `nodeRules[].name` /
16
+ * `childNodeRules[].name` (selector-scoped named groups), as opposed to
17
+ * {@link baseRuleToVirtualNames}'s top-level `rules` groups. Because these wrappers
18
+ * DO have their own selector scope, only a disable (`false`) may be propagated to
19
+ * them from an unrelated nodeRules/childNodeRules entry — forwarding a non-false
20
+ * value would apply the wrapper's semantics to nodes its own selector never matched
21
+ * (see issue #4023's fix history).
22
+ */
23
+ baseRuleToScopedVirtualNames;
13
24
  /** Rule overrides that apply to child nodes matching specific selectors */
14
25
  childNodeRules;
26
+ /**
27
+ * `rules` keys that are a genuine NamedRuleGroup in some layer of the
28
+ * `extends`/override chain, carried over from {@link Config.knownNamedRuleGroupKeys}
29
+ * even where the final merged value collapsed to `false`. See that field's JSDoc.
30
+ */
31
+ knownNamedRuleGroupKeys;
15
32
  /**
16
33
  * Errors collected during rule mapping (e.g., invalid wildcard usage).
17
34
  * Consumed by MLCore and reported as config-error violations.
@@ -23,12 +40,15 @@ export class Ruleset {
23
40
  rules;
24
41
  /**
25
42
  * @param config - The markuplint configuration to extract rules from
26
- * @param baseRuleToVirtualNames - Mapping from base rule names to virtual rule names
43
+ * @param baseRuleToVirtualNames - Mapping from base rule names to top-level-group virtual rule names
44
+ * @param baseRuleToScopedVirtualNames - Mapping from base rule names to selector-scoped (nodeRules/childNodeRules) virtual rule names
27
45
  */
28
- constructor(config, baseRuleToVirtualNames) {
46
+ constructor(config, baseRuleToVirtualNames, baseRuleToScopedVirtualNames) {
29
47
  this.rules = config.rules ?? {};
30
48
  this.nodeRules = config.nodeRules ?? [];
31
49
  this.childNodeRules = config.childNodeRules ?? [];
32
50
  this.baseRuleToVirtualNames = baseRuleToVirtualNames ?? new Map();
51
+ this.baseRuleToScopedVirtualNames = baseRuleToScopedVirtualNames ?? new Map();
52
+ this.knownNamedRuleGroupKeys = new Set(config.knownNamedRuleGroupKeys);
33
53
  }
34
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/ml-core",
3
- "version": "5.0.0-rc.6",
3
+ "version": "5.0.0-rc.7",
4
4
  "description": "The core module of markuplint",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,20 +34,20 @@
34
34
  "./lib/configs.js": "./lib/configs.browser.js"
35
35
  },
36
36
  "dependencies": {
37
- "@markuplint/config-presets": "5.0.0-rc.6",
38
- "@markuplint/html-parser": "5.0.0-rc.6",
39
- "@markuplint/html-spec": "5.0.0-rc.6",
40
- "@markuplint/i18n": "5.0.0-rc.6",
41
- "@markuplint/ml-ast": "5.0.0-rc.6",
42
- "@markuplint/ml-config": "5.0.0-rc.6",
43
- "@markuplint/ml-spec": "5.0.0-rc.6",
44
- "@markuplint/parser-utils": "5.0.0-rc.6",
45
- "@markuplint/selector": "5.0.0-rc.6",
46
- "@markuplint/shared": "5.0.0-rc.6",
37
+ "@markuplint/config-presets": "5.0.0-rc.7",
38
+ "@markuplint/html-parser": "5.0.0-rc.7",
39
+ "@markuplint/html-spec": "5.0.0-rc.7",
40
+ "@markuplint/i18n": "5.0.0-rc.7",
41
+ "@markuplint/ml-ast": "5.0.0-rc.7",
42
+ "@markuplint/ml-config": "5.0.0-rc.7",
43
+ "@markuplint/ml-spec": "5.0.0-rc.7",
44
+ "@markuplint/parser-utils": "5.0.0-rc.7",
45
+ "@markuplint/selector": "5.0.0-rc.7",
46
+ "@markuplint/shared": "5.0.0-rc.7",
47
47
  "@types/debug": "4.1.13",
48
48
  "debug": "4.4.3",
49
49
  "is-plain-object": "5.0.0",
50
50
  "type-fest": "5.6.0"
51
51
  },
52
- "gitHead": "c02c3a0783eac6b2fb4707be2dc00b88f6219641"
52
+ "gitHead": "8d7a3b801e0f5f88438c003e7eee2bd45e6aedd0"
53
53
  }