@cssdoc/providers 0.2.0 → 0.3.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/dist/index.d.mts CHANGED
@@ -10,7 +10,7 @@ type RuleSeverity = "off" | "warn" | "error";
10
10
  * author-side `RuleName` (which omits the consumer-side `unknown-modifier`/`deprecated-modifier` and
11
11
  * the opt-in `unknown-custom-property`).
12
12
  */
13
- type RuleId = "missing-summary" | "undocumented-modifier" | "deprecated-requires-canonical" | "name-not-in-css" | "unknown-modifier" | "deprecated-modifier" | "undocumented-part" | "invalid-default-value" | "invalid-property-value" | "invalid-fallback-value" | "unknown-custom-property";
13
+ type RuleId = "missing-summary" | "undocumented-modifier" | "deprecated-requires-canonical" | "name-not-in-css" | "unknown-modifier" | "deprecated-modifier" | "unknown-state" | "unknown-part" | "undocumented-part" | "undocumented-css-part" | "component-name-case" | "part-name-case" | "structure-unknown-selector" | "invalid-default-value" | "invalid-property-value" | "invalid-fallback-value" | "unknown-custom-property" | "cssdoc-directive";
14
14
  /** A resolved severity for every rule. */
15
15
  type RuleSeverities = Record<RuleId, RuleSeverity>;
16
16
  /**
@@ -65,6 +65,27 @@ interface UsageOptions {
65
65
  */
66
66
  propertyPrefix?: string;
67
67
  }
68
+ /**
69
+ * A required name case: a built-in preset, or a custom regular-expression source string tested
70
+ * against the class name. `(string & {})` keeps preset autocomplete while accepting any pattern.
71
+ */
72
+ type NameCase = "pascalCase" | "camelCase" | "lowercase" | (string & {});
73
+ /** Which class names a `naming` convention enforces. Absent members aren't checked. */
74
+ interface NamingRules {
75
+ /** The component base-class name (e.g. SUIT `Card` → `pascalCase`). */
76
+ component?: NameCase;
77
+ /** Sub-element part class names. */
78
+ part?: NameCase;
79
+ }
80
+ /** The built-in name-case patterns, tested against a class name without its leading dot. */
81
+ declare const NAME_CASE_PRESETS: Record<string, RegExp>;
82
+ /** A {@link NamingRules} compiled to regexes (custom patterns compiled from their source). */
83
+ interface ResolvedNaming {
84
+ component?: RegExp;
85
+ part?: RegExp;
86
+ }
87
+ /** Compile {@link NamingRules} to regexes once, for the record/part name-case checks. */
88
+ declare function resolveNaming(naming?: NamingRules): ResolvedNaming;
68
89
  //#endregion
69
90
  //#region src/syntax.d.ts
70
91
  /** The result of matching a value against a syntax. */
@@ -84,9 +105,31 @@ interface SyntaxMatch {
84
105
  */
85
106
  declare function matchesSyntax(syntax: string, value: string): SyntaxMatch;
86
107
  //#endregion
108
+ //#region src/directives.d.ts
109
+ type DirectiveKind = "disable" | "enable" | "disable-line" | "disable-next-line" | "expect-error";
110
+ interface Directive {
111
+ kind: DirectiveKind;
112
+ /** Rule ids the directive scopes to, or `null` for every rule. */
113
+ rules: string[] | null;
114
+ /** 1-based line the comment starts / ends on. */
115
+ startLine: number;
116
+ endLine: number;
117
+ }
118
+ /** Parse every `cssdoc-*` directive comment out of a CSS source. */
119
+ declare function parseDirectives(source: string): Directive[];
120
+ /**
121
+ * Drop diagnostics suppressed by directive comments in `source`, and add a `cssdoc-directive`
122
+ * diagnostic for any `cssdoc-expect-error` that matched nothing.
123
+ *
124
+ * @param diagnostics - The diagnostics to filter (author-side, with CSS spans).
125
+ * @param source - The CSS the diagnostics came from (the same text passed to the linter).
126
+ * @returns The surviving diagnostics.
127
+ */
128
+ declare function applyDirectives(diagnostics: Diagnostic[], source: string): Diagnostic[];
129
+ //#endregion
87
130
  //#region src/aspects.d.ts
88
131
  declare const record: {
89
- model(index: CssDocIndex): Diagnostic[];
132
+ model(index: CssDocIndex, naming?: ResolvedNaming, structureIgnore?: readonly string[]): Diagnostic[];
90
133
  completions(index: CssDocIndex): Completion[];
91
134
  hover(base: string, index: CssDocIndex): Hover | undefined;
92
135
  definition(base: string, index: CssDocIndex): Location$1 | undefined;
@@ -99,8 +142,15 @@ declare const modifier: {
99
142
  definition(base: string, token: string, index: CssDocIndex): Location$1 | undefined;
100
143
  };
101
144
  declare const part: {
145
+ model(index: CssDocIndex, naming?: ResolvedNaming): Diagnostic[];
146
+ };
147
+ declare const cssPart: {
102
148
  model(index: CssDocIndex): Diagnostic[];
103
149
  };
150
+ /** A host-document class that looks like a state (via `statePrefixes`) but isn't a documented state. */
151
+ declare function stateUsage(usage: ClassUsage, index: CssDocIndex): Diagnostic[];
152
+ /** A host-document class that looks like a BEM element (`base<sep>…`) but isn't a documented part. */
153
+ declare function partUsage(usage: ClassUsage, index: CssDocIndex): Diagnostic[];
104
154
  declare const customProperty: {
105
155
  /** Author-side: a registered property's default (`initial-value`/`@defaultValue`) must match its syntax. */model(index: CssDocIndex): Diagnostic[]; /** Consumer-side: an assignment `--name: value` must match the property's declared syntax. */
106
156
  assignment(a: PropertyAssignment, index: CssDocIndex): Diagnostic[];
@@ -117,12 +167,15 @@ declare const func: {
117
167
  //#endregion
118
168
  //#region src/index.d.ts
119
169
  /** The aspect names covered, in a stable order (the extension point for future aspects). */
120
- declare const ASPECTS: readonly ["record", "modifier", "part", "custom-property", "structure", "function", "state", "condition"];
170
+ declare const ASPECTS: readonly ["record", "modifier", "part", "css-part", "custom-property", "structure", "function", "state", "condition"];
121
171
  /** Author-side hygiene diagnostics over the whole model (missing summaries, undocumented members, drift, invalid defaults). */
122
- declare function lintModel(index: CssDocIndex, severities?: RuleSeverities): Diagnostic[];
172
+ declare function lintModel(index: CssDocIndex, severities?: RuleSeverities, naming?: ResolvedNaming, structureIgnore?: readonly string[]): Diagnostic[];
123
173
  /** Consumer-side diagnostics for custom-property assignments (`--x: value` must match its `@property` syntax). */
124
174
  declare function checkPropertyAssignments(assignments: readonly PropertyAssignment[], index: CssDocIndex, severities?: RuleSeverities): Diagnostic[];
125
- /** Consumer-side diagnostics for class-attribute usage (unknown or deprecated modifiers). */
175
+ /**
176
+ * Consumer-side diagnostics for class-attribute usage: unknown/deprecated modifiers, plus unknown
177
+ * state classes (`statePrefixes`) and unknown BEM element classes, routed by the token's kind.
178
+ */
126
179
  declare function checkClassUsage(usages: readonly ClassUsage[], index: CssDocIndex, severities?: RuleSeverities): Diagnostic[];
127
180
  /** Consumer-side diagnostics for `var(--…)` references (unknown custom properties; opt-in via prefix). */
128
181
  declare function checkPropertyUsage(usages: readonly PropertyUsage[], index: CssDocIndex, options?: UsageOptions, severities?: RuleSeverities): Diagnostic[];
@@ -145,4 +198,4 @@ declare function definitionForCustomProperty(name: string, index: CssDocIndex):
145
198
  /** Definition of a custom function (its `@function` rule). */
146
199
  declare function definitionForFunction(name: string, index: CssDocIndex): Location$1 | undefined;
147
200
  //#endregion
148
- export { ASPECTS, Completion, CompletionKind, DEFAULT_RULE_SEVERITIES, Diagnostic, Hover, type Location, RuleId, RuleSeverities, RuleSeverity, Severity, type SourceSpan, SyntaxMatch, UsageOptions, checkClassUsage, checkPropertyAssignments, checkPropertyUsage, completeClasses, completeCustomProperties, completeFunctions, customProperty, definitionForClass, definitionForCustomProperty, definitionForFunction, func, hoverForClass, hoverForCustomProperty, hoverForFunction, lintModel, matchesSyntax, modifier, part, record, resolveRuleSeverities };
201
+ export { ASPECTS, Completion, CompletionKind, DEFAULT_RULE_SEVERITIES, Diagnostic, Hover, type Location, NAME_CASE_PRESETS, NameCase, NamingRules, ResolvedNaming, RuleId, RuleSeverities, RuleSeverity, Severity, type SourceSpan, SyntaxMatch, UsageOptions, applyDirectives, checkClassUsage, checkPropertyAssignments, checkPropertyUsage, completeClasses, completeCustomProperties, completeFunctions, cssPart, customProperty, definitionForClass, definitionForCustomProperty, definitionForFunction, func, hoverForClass, hoverForCustomProperty, hoverForFunction, lintModel, matchesSyntax, modifier, parseDirectives, part, partUsage, record, resolveNaming, resolveRuleSeverities, stateUsage };
package/dist/index.mjs CHANGED
@@ -56,29 +56,69 @@ function matchesSyntax(syntax, value) {
56
56
  }
57
57
  //#endregion
58
58
  //#region src/aspects.ts
59
- /**
60
- * The aspect modules. Each documented aspect contributes some of: author-side model diagnostics,
61
- * consumer-side usage diagnostics, completions, hover, and definitions. All six aspects the model
62
- * carries are represented; the component hover folds in the structure, state, and condition facts, and
63
- * the generators (phase 3) surface them further. `index.ts` aggregates these into the public API.
64
- *
65
- * @module
66
- */
67
59
  const stripDot = (name) => name.replace(/^\./u, "");
68
60
  const warn = (d) => ({
69
61
  ...d,
70
62
  severity: "warning"
71
63
  });
64
+ /**
65
+ * Match a class name against a `structureIgnore` pattern — a literal name or a simple glob where `*`
66
+ * stands for any run of characters (e.g. `util-*`, `*--legacy`, `*`). Matched literally otherwise.
67
+ */
68
+ const globMatch = (pattern, value) => {
69
+ if (!pattern.includes("*")) return pattern === value;
70
+ return new RegExp(`^${pattern.replace(/[.+?^${}()|[\]\\]/gu, "\\$&").replace(/\*/gu, ".*")}$`, "u").test(value);
71
+ };
72
72
  const record = {
73
- model(index) {
73
+ model(index, naming, structureIgnore = []) {
74
74
  const out = [];
75
- for (const info of index.records) if (!info.entry.summary?.trim()) out.push(warn({
76
- aspect: "record",
77
- rule: "missing-summary",
78
- message: `Record "${info.entry.name}" has no @summary.`,
79
- record: info.entry.name,
80
- span: info.span
81
- }));
75
+ for (const info of index.records) {
76
+ if (!info.entry.summary?.trim()) out.push(warn({
77
+ aspect: "record",
78
+ rule: "missing-summary",
79
+ message: `Record "${info.entry.name}" has no @summary.`,
80
+ record: info.entry.name,
81
+ span: info.span
82
+ }));
83
+ if (naming?.component && info.entry.kind === "component" && info.entry.className) {
84
+ if (!naming.component.test(stripDot(info.entry.className))) out.push(warn({
85
+ aspect: "record",
86
+ rule: "component-name-case",
87
+ message: `Component class "${info.entry.className}" doesn't match the configured name case.`,
88
+ record: info.entry.name,
89
+ span: info.span
90
+ }));
91
+ }
92
+ if (info.entry.structure?.length) {
93
+ const known = /* @__PURE__ */ new Set([
94
+ stripDot(info.entry.className),
95
+ ...info.entry.parts.flatMap((p) => [stripDot(p.name), ...(p.modifiers ?? []).map((m) => m.name)]),
96
+ ...info.entry.shadowParts.map((p) => stripDot(p.name)),
97
+ ...info.entry.states.map((s) => s.name),
98
+ ...info.entry.modifiers.map((m) => m.name),
99
+ ...info.entry.slots.map((s) => s.name)
100
+ ]);
101
+ const seen = /* @__PURE__ */ new Set();
102
+ const walk = (nodes) => {
103
+ for (const node of nodes) {
104
+ for (const m of node.selector.matchAll(/\.([\w-]+)/gu)) {
105
+ const cls = m[1];
106
+ if (seen.has(cls) || known.has(cls) || structureIgnore.some((g) => globMatch(g, cls))) continue;
107
+ seen.add(cls);
108
+ out.push(warn({
109
+ aspect: "record",
110
+ rule: "structure-unknown-selector",
111
+ message: `@structure references ".${cls}", which isn't the component class or a documented member (add it, or list it under structureIgnore).`,
112
+ record: info.entry.name,
113
+ span: info.span
114
+ }));
115
+ }
116
+ walk(node.children);
117
+ }
118
+ };
119
+ walk(info.entry.structure);
120
+ }
121
+ }
82
122
  return out;
83
123
  },
84
124
  completions(index) {
@@ -99,6 +139,7 @@ const record = {
99
139
  const facets = [
100
140
  entry.modifiers.length && `${entry.modifiers.length} modifiers`,
101
141
  entry.parts.length && `${entry.parts.length} parts`,
142
+ entry.shadowParts.length && `${entry.shadowParts.length} shadow parts`,
102
143
  entry.states.length && `${entry.states.length} states`,
103
144
  entry.cssPropertiesDeclared.length && `${entry.cssPropertiesDeclared.length} custom properties`,
104
145
  entry.functions.length && `${entry.functions.length} functions`,
@@ -199,17 +240,26 @@ const modifier = {
199
240
  return entry ? index.location(entry.name, memberKey("modifier", index.matcher.normalizeMember(token))) : void 0;
200
241
  }
201
242
  };
202
- const part = { model(index) {
243
+ const part = { model(index, naming) {
203
244
  const out = [];
204
245
  for (const info of index.records) {
205
246
  const name = info.entry.name;
206
- for (const p of info.entry.parts) if (!p.description?.trim()) out.push(warn({
207
- aspect: "part",
208
- rule: "undocumented-part",
209
- message: `Part ".${p.name}" of "${name}" has no @part description.`,
210
- record: name,
211
- span: info.memberSpans.get(memberKey("part", p.name)) ?? info.span
212
- }));
247
+ for (const p of info.entry.parts) {
248
+ if (!p.description?.trim()) out.push(warn({
249
+ aspect: "part",
250
+ rule: "undocumented-part",
251
+ message: `Part ".${p.name}" of "${name}" has no @part description.`,
252
+ record: name,
253
+ span: info.memberSpans.get(memberKey("part", p.name)) ?? info.span
254
+ }));
255
+ if (naming?.part && !naming.part.test(p.name)) out.push(warn({
256
+ aspect: "part",
257
+ rule: "part-name-case",
258
+ message: `Part ".${p.name}" of "${name}" doesn't match the configured name case.`,
259
+ record: name,
260
+ span: info.memberSpans.get(memberKey("part", p.name)) ?? info.span
261
+ }));
262
+ }
213
263
  for (const authored of info.authoredParts) if (!info.selectorText.includes(`.${authored}`)) out.push(warn({
214
264
  aspect: "part",
215
265
  rule: "name-not-in-css",
@@ -220,6 +270,50 @@ const part = { model(index) {
220
270
  }
221
271
  return out;
222
272
  } };
273
+ const cssPart = { model(index) {
274
+ const out = [];
275
+ for (const info of index.records) {
276
+ const name = info.entry.name;
277
+ for (const p of info.entry.shadowParts) if (!p.description?.trim()) out.push(warn({
278
+ aspect: "css-part",
279
+ rule: "undocumented-css-part",
280
+ message: `Shadow part "::part(${p.name})" of "${name}" has no @csspart description.`,
281
+ record: name,
282
+ span: info.memberSpans.get(memberKey("shadow-part", p.name)) ?? info.span
283
+ }));
284
+ }
285
+ return out;
286
+ } };
287
+ /** A host-document class that looks like a state (via `statePrefixes`) but isn't a documented state. */
288
+ function stateUsage(usage, index) {
289
+ if (!usage.base) return [];
290
+ const entry = index.componentForClass(usage.base);
291
+ if (!entry) return [];
292
+ const name = usage.token.replace(/^\./u, "");
293
+ if (entry.states.some((s) => s.name === name)) return [];
294
+ return [warn({
295
+ aspect: "state",
296
+ rule: "unknown-state",
297
+ message: `".${name}" is not a documented state of "${entry.name}".`,
298
+ record: entry.name,
299
+ span: usage.loc
300
+ })];
301
+ }
302
+ /** A host-document class that looks like a BEM element (`base<sep>…`) but isn't a documented part. */
303
+ function partUsage(usage, index) {
304
+ if (!usage.base) return [];
305
+ const entry = index.componentForClass(usage.base);
306
+ if (!entry) return [];
307
+ const name = usage.token.replace(/^\./u, "");
308
+ if (entry.parts.some((p) => p.name === name || p.modifiers?.some((m) => m.name === name))) return [];
309
+ return [warn({
310
+ aspect: "part",
311
+ rule: "unknown-part",
312
+ message: `".${name}" is not a documented part of "${entry.name}".`,
313
+ record: entry.name,
314
+ span: usage.loc
315
+ })];
316
+ }
223
317
  function findProperty(index, name) {
224
318
  for (const rec of index.records) {
225
319
  const i = rec.entry.cssPropertiesDeclared.findIndex((p) => p.name === name);
@@ -343,11 +437,18 @@ const DEFAULT_RULE_SEVERITIES = {
343
437
  "name-not-in-css": "warn",
344
438
  "unknown-modifier": "warn",
345
439
  "deprecated-modifier": "warn",
440
+ "unknown-state": "warn",
441
+ "unknown-part": "warn",
346
442
  "undocumented-part": "warn",
443
+ "undocumented-css-part": "warn",
444
+ "component-name-case": "warn",
445
+ "part-name-case": "warn",
446
+ "structure-unknown-selector": "warn",
347
447
  "invalid-default-value": "warn",
348
448
  "invalid-property-value": "warn",
349
449
  "invalid-fallback-value": "warn",
350
- "unknown-custom-property": "off"
450
+ "unknown-custom-property": "off",
451
+ "cssdoc-directive": "warn"
351
452
  };
352
453
  /**
353
454
  * Merge per-rule overrides over {@link DEFAULT_RULE_SEVERITIES}. A `boolean` override is accepted for
@@ -363,6 +464,120 @@ function resolveRuleSeverities(overrides) {
363
464
  }
364
465
  return resolved;
365
466
  }
467
+ /** The built-in name-case patterns, tested against a class name without its leading dot. */
468
+ const NAME_CASE_PRESETS = {
469
+ pascalCase: /^[A-Z][A-Za-z0-9]*$/u,
470
+ camelCase: /^[a-z][A-Za-z0-9]*$/u,
471
+ lowercase: /^[a-z][a-z0-9-]*$/u
472
+ };
473
+ /** Compile one {@link NameCase} to a regex: a preset, else the string as a custom pattern. */
474
+ function compileNameCase(spec) {
475
+ if (!spec) return void 0;
476
+ if (spec in NAME_CASE_PRESETS) return NAME_CASE_PRESETS[spec];
477
+ try {
478
+ return new RegExp(spec, "u");
479
+ } catch {
480
+ return;
481
+ }
482
+ }
483
+ /** Compile {@link NamingRules} to regexes once, for the record/part name-case checks. */
484
+ function resolveNaming(naming) {
485
+ return {
486
+ component: compileNameCase(naming?.component),
487
+ part: compileNameCase(naming?.part)
488
+ };
489
+ }
490
+ //#endregion
491
+ //#region src/directives.ts
492
+ const DIRECTIVE_RE = /\/\*\s*cssdoc-(disable-next-line|disable-line|disable|enable|expect-error)\b([^*]*?)\*\//gu;
493
+ const lineAt = (source, index) => {
494
+ let line = 1;
495
+ for (let i = 0; i < index && i < source.length; i++) if (source[i] === "\n") line += 1;
496
+ return line;
497
+ };
498
+ /** Parse every `cssdoc-*` directive comment out of a CSS source. */
499
+ function parseDirectives(source) {
500
+ const out = [];
501
+ for (const m of source.matchAll(DIRECTIVE_RE)) {
502
+ const at = m.index ?? 0;
503
+ const rest = m[2].trim();
504
+ out.push({
505
+ kind: m[1],
506
+ rules: rest ? rest.split(/[\s,]+/u).filter(Boolean) : null,
507
+ startLine: lineAt(source, at),
508
+ endLine: lineAt(source, at + m[0].length - 1)
509
+ });
510
+ }
511
+ return out;
512
+ }
513
+ const scopesRule = (d, rule) => d.rules === null || d.rules.includes(rule);
514
+ /**
515
+ * Drop diagnostics suppressed by directive comments in `source`, and add a `cssdoc-directive`
516
+ * diagnostic for any `cssdoc-expect-error` that matched nothing.
517
+ *
518
+ * @param diagnostics - The diagnostics to filter (author-side, with CSS spans).
519
+ * @param source - The CSS the diagnostics came from (the same text passed to the linter).
520
+ * @returns The surviving diagnostics.
521
+ */
522
+ function applyDirectives(diagnostics, source) {
523
+ const directives = parseDirectives(source);
524
+ if (directives.length === 0) return diagnostics;
525
+ const block = directives.filter((d) => d.kind === "disable" || d.kind === "enable").sort((a, b) => a.startLine - b.startLine);
526
+ const perLine = directives.filter((d) => d.kind !== "disable" && d.kind !== "enable");
527
+ const blockDisabled = (line, rule) => {
528
+ let all = false;
529
+ const disabled = /* @__PURE__ */ new Set();
530
+ const exceptions = /* @__PURE__ */ new Set();
531
+ for (const d of block) {
532
+ if (d.startLine > line) break;
533
+ if (d.kind === "disable" && d.rules === null) {
534
+ all = true;
535
+ exceptions.clear();
536
+ } else if (d.kind === "disable") for (const r of d.rules ?? []) if (all) exceptions.delete(r);
537
+ else disabled.add(r);
538
+ else if (d.rules === null) {
539
+ all = false;
540
+ disabled.clear();
541
+ exceptions.clear();
542
+ } else for (const r of d.rules) if (all) exceptions.add(r);
543
+ else disabled.delete(r);
544
+ }
545
+ return all ? !exceptions.has(rule) : disabled.has(rule);
546
+ };
547
+ const lineDirectiveFor = (line, rule) => perLine.find((d) => scopesRule(d, rule) && (d.kind === "disable-line" ? line >= d.startLine && line <= d.endLine : line === d.endLine + 1));
548
+ const satisfied = /* @__PURE__ */ new Set();
549
+ const kept = [];
550
+ for (const diag of diagnostics) {
551
+ const line = diag.span?.start.line;
552
+ if (line === void 0) {
553
+ kept.push(diag);
554
+ continue;
555
+ }
556
+ const hit = lineDirectiveFor(line, diag.rule);
557
+ if (hit) {
558
+ satisfied.add(hit);
559
+ continue;
560
+ }
561
+ if (!blockDisabled(line, diag.rule)) kept.push(diag);
562
+ }
563
+ for (const d of perLine) if (d.kind === "expect-error" && !satisfied.has(d)) kept.push({
564
+ aspect: "directive",
565
+ rule: "cssdoc-directive",
566
+ message: "Unused cssdoc-expect-error: no cssdoc problem was reported on the following line.",
567
+ span: {
568
+ start: {
569
+ line: d.startLine,
570
+ column: 1
571
+ },
572
+ end: {
573
+ line: d.endLine,
574
+ column: 2
575
+ }
576
+ },
577
+ severity: "warning"
578
+ });
579
+ return kept;
580
+ }
366
581
  //#endregion
367
582
  //#region src/index.ts
368
583
  /**
@@ -387,6 +602,7 @@ const ASPECTS = [
387
602
  "record",
388
603
  "modifier",
389
604
  "part",
605
+ "css-part",
390
606
  "custom-property",
391
607
  "structure",
392
608
  "function",
@@ -394,11 +610,12 @@ const ASPECTS = [
394
610
  "condition"
395
611
  ];
396
612
  /** Author-side hygiene diagnostics over the whole model (missing summaries, undocumented members, drift, invalid defaults). */
397
- function lintModel(index, severities = DEFAULT_RULE_SEVERITIES) {
613
+ function lintModel(index, severities = DEFAULT_RULE_SEVERITIES, naming, structureIgnore) {
398
614
  return applySeverities([
399
- ...record.model(index),
615
+ ...record.model(index, naming, structureIgnore),
400
616
  ...modifier.model(index),
401
- ...part.model(index),
617
+ ...part.model(index, naming),
618
+ ...cssPart.model(index),
402
619
  ...customProperty.model(index)
403
620
  ], severities);
404
621
  }
@@ -406,9 +623,17 @@ function lintModel(index, severities = DEFAULT_RULE_SEVERITIES) {
406
623
  function checkPropertyAssignments(assignments, index, severities = DEFAULT_RULE_SEVERITIES) {
407
624
  return applySeverities(assignments.flatMap((a) => customProperty.assignment(a, index)), severities);
408
625
  }
409
- /** Consumer-side diagnostics for class-attribute usage (unknown or deprecated modifiers). */
626
+ /**
627
+ * Consumer-side diagnostics for class-attribute usage: unknown/deprecated modifiers, plus unknown
628
+ * state classes (`statePrefixes`) and unknown BEM element classes, routed by the token's kind.
629
+ */
410
630
  function checkClassUsage(usages, index, severities = DEFAULT_RULE_SEVERITIES) {
411
- return applySeverities(usages.flatMap((usage) => modifier.classUsage(usage, index)), severities);
631
+ return applySeverities(usages.flatMap((usage) => {
632
+ const kind = usage.base ? index.matcher.usageKind(usage.token, usage.base) : void 0;
633
+ if (kind === "state") return stateUsage(usage, index);
634
+ if (kind === "element") return partUsage(usage, index);
635
+ return modifier.classUsage(usage, index);
636
+ }), severities);
412
637
  }
413
638
  /** Consumer-side diagnostics for `var(--…)` references (unknown custom properties; opt-in via prefix). */
414
639
  function checkPropertyUsage(usages, index, options = {}, severities = DEFAULT_RULE_SEVERITIES) {
@@ -451,4 +676,4 @@ function definitionForFunction(name, index) {
451
676
  return func.definition(name, index);
452
677
  }
453
678
  //#endregion
454
- export { ASPECTS, DEFAULT_RULE_SEVERITIES, checkClassUsage, checkPropertyAssignments, checkPropertyUsage, completeClasses, completeCustomProperties, completeFunctions, customProperty, definitionForClass, definitionForCustomProperty, definitionForFunction, func, hoverForClass, hoverForCustomProperty, hoverForFunction, lintModel, matchesSyntax, modifier, part, record, resolveRuleSeverities };
679
+ export { ASPECTS, DEFAULT_RULE_SEVERITIES, NAME_CASE_PRESETS, applyDirectives, checkClassUsage, checkPropertyAssignments, checkPropertyUsage, completeClasses, completeCustomProperties, completeFunctions, cssPart, customProperty, definitionForClass, definitionForCustomProperty, definitionForFunction, func, hoverForClass, hoverForCustomProperty, hoverForFunction, lintModel, matchesSyntax, modifier, parseDirectives, part, partUsage, record, resolveNaming, resolveRuleSeverities, stateUsage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cssdoc/providers",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Host-agnostic aspect providers over the @cssdoc/index — diagnostics, completions, hover, and definitions for modifiers, custom properties, structure, functions, states, and conditions.",
5
5
  "keywords": [
6
6
  "css",
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "css-tree": "^3.2.1",
32
- "@cssdoc/core": "0.2.0",
33
- "@cssdoc/index": "0.2.0"
32
+ "@cssdoc/core": "0.3.0",
33
+ "@cssdoc/index": "0.3.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/css-tree": "^2.3.11",