Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | 4x 4x 4x 4x 4x 76x 286x 68x 8x 4x 68x 68x 65x 65x 3x 3x 62x 62x 7x 7x 5x 8x 4x 60x 2x 2x 4x 89x 76x 204x 204x 18x 58x | /**
* Component-hook naming anti-patterns rejected by RFC 1157.
*
* Detection runs before the structural validator so that hooks following
* a known-bad shape get a clear rename hint, not a generic
* `category=...` / `property=...` error from the segment walker.
*
* Each detector returns a {@link ValidationError} with a `hint` describing
* the canonical replacement. Order matters: the first anti-pattern that
* matches wins (single-finding-per-hook keeps reporting digestible).
*
* Coverage:
*
* 1. **Property-first color** — RFC 1157 §Naming Doctrine (lines 41-78)
* retires `-text-color`, `-bg-color`, `-background-color`,
* `-border-color` in favor of category-last forms
* (`-color-foreground`, `-color-background`, `-color-border`).
*
* 2. **Bare `-height` / `-width`** — RFC 1157 §Sizing requires the
* `-sizing-` segment (`--slds-c-foo-sizing-height`). Bare names and
* reversed `-min-{width|height}` / `-max-{width|height}` legacy
* property shapes are rejected.
*
* 3. **Retired `-context-` infix** — early-draft RFC reserved
* `--slds-c-{C}-context-{property}` for child-side fallback tiers;
* retired in the final RFC.
*
* The detector operates on the full hook string (`--slds-c-...`), not on
* tokenized segments — anti-patterns are most readable matched against
* the literal name a human typed.
*/
import type { ValidationError } from '../../types.js';
interface AntiPatternRule {
/** Stable identifier surfaced as `error.segment`. */
id: string;
/** Predicate-and-rename: returns the canonical form, or `null` to skip. */
rewrite(hook: string): string | null;
/** Build the rename hint shown to the user. */
hint(hook: string, canonical: string): string;
}
const PROPERTY_FIRST_REPLACEMENTS: Array<[RegExp, string]> = [
[/-text-color(?=-|$)/, '-color-foreground'],
[/-background-color(?=-|$)/, '-color-background'],
[/-bg-color(?=-|$)/, '-color-background'],
[/-border-color(?=-|$)/, '-color-border'],
];
/** Match a `--slds-c-*` hook ending in `-height` / `-width` (with optional `-min` / `-max`). */
const BARE_SIZING_RE = /^(--slds-c-[a-z0-9-]+?)-(height|width)(-min|-max)?$/;
/** Match the reversed legacy `-{min|max}-{width|height}` property shape. */
const REVERSED_MINMAX_RE = /^(--slds-c-[a-z0-9-]+?)-(min|max)-(height|width)$/;
/** Carveouts for bare `-{height|width}` checks. */
const BARE_SIZING_CARVEOUTS = [/-line-height$/];
const PROPERTY_FIRST_COLOR: AntiPatternRule = {
id: 'naming-property-first-color',
rewrite(hook) {
for (const [pattern, replacement] of PROPERTY_FIRST_REPLACEMENTS) {
if (pattern.test(hook)) return hook.replace(pattern, replacement);
}
return null;
},
hint(hook, canonical) {
return (
`Color hook \`${hook}\` is property-first; rename to \`${canonical}\`. ` +
`Per RFC 1157, color hook names put the category segment last: ` +
`\`-color-foreground\`, \`-color-background\`, or \`-color-border\`.`
);
},
};
const BARE_SIZING: AntiPatternRule = {
id: 'naming-bare-sizing',
rewrite(hook) {
for (const carveout of BARE_SIZING_CARVEOUTS) {
if (carveout.test(hook)) return null;
}
const reversed = REVERSED_MINMAX_RE.exec(hook);
if (reversed) {
const [, body, minmax, dim] = reversed;
// `body` already ends in `-sizing` for `--slds-c-foo-sizing-min-height`;
// otherwise we need to inject the segment.
return body.endsWith('-sizing') ? `${body}-${dim}-${minmax}` : `${body}-sizing-${dim}-${minmax}`;
}
const m = BARE_SIZING_RE.exec(hook);
if (!m) return null;
const [, body, dim, tail] = m;
if (body.endsWith('-sizing')) return null; // already canonical
return `${body}-sizing-${dim}${tail ?? ''}`;
},
hint(hook, canonical) {
return (
`Sizing hook \`${hook}\` is missing the \`-sizing-\` segment; rename to \`${canonical}\`. ` +
`Per RFC 1157, per-axis physical sizing hooks are \`--slds-c-{C}-[{E}-]sizing-{height|width}\` ` +
`(with optional terminal \`-min\` / \`-max\`).`
);
},
};
const RETIRED_CONTEXT_INFIX: AntiPatternRule = {
id: 'naming-retired-context-infix',
rewrite(hook) {
if (!/-context-/.test(hook)) return null;
return hook.replace(/-context-/, '-');
},
hint(hook, canonical) {
return (
`Hook \`${hook}\` uses the retired \`-context-\` infix; rename to \`${canonical}\`. ` +
`RFC 1157 retired the infix; context hooks are now parent-scoped ` +
`(declared by the parent component on a descendant selector).`
);
},
};
/** All anti-pattern rules in detection priority. First match wins. */
const RULES: AntiPatternRule[] = [PROPERTY_FIRST_COLOR, BARE_SIZING, RETIRED_CONTEXT_INFIX];
/**
* Return the first matching anti-pattern error for `hook`, or `null` if
* none of the documented bad shapes apply. The validator runs this
* before structural parsing.
*/
export function detectAntiPattern(hook: string): ValidationError | null {
if (!hook.startsWith('--slds-c-') && !hook.startsWith('--slds-s-')) return null;
for (const rule of RULES) {
const canonical = rule.rewrite(hook);
if (canonical) {
return {
segment: rule.id,
expected: [canonical],
received: hook,
hint: rule.hint(hook, canonical),
};
}
}
return null;
}
|