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 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | 68x 62x 4x 150x 39x 39x 17x 17x 65x 65x 65x 65x 65x 65x 3x 3x 62x 2x 2x 60x 60x 3x 60x 60x 60x 3x 3x 57x 57x 57x 65x 7x 3x 3x 50x 50x 45x 45x 5x 2x 2x 52x 52x 65x 65x 3x 3x 49x 49x 49x 4x 49x | /**
* Component and shared token validator.
*
* Pattern: --{ns}-{c|s}-{component}[-{element}]-{category}-{property}[-{attribute}][-{state}][-{pseudoState}]
*
* Component hooks (--slds-c-*) and shared tokens (--slds-s-*) follow the
* same naming structure. The scope character is a parameter, not a branching
* condition.
*
* @module validators/component
*/
import type { Metadata, ValidationResult, ValidationError, ValidationWarning } from '../../types.js';
import type { TokenMatch } from '../shared/helpers.js';
import { err, matchToken } from '../shared/helpers.js';
import { detectAntiPattern } from './antiPatterns.js';
function findCategoryIndex(rest: string[], categories: string[]): number {
return rest.findIndex((seg) => categories.includes(seg));
}
function addLegacyWarning(warnings: ValidationWarning[], segment: string, m: TokenMatch): void {
if (m.isLegacy && m.match) {
warnings.push({
type: 'legacy-syntax',
segment,
received: m.received ?? '',
canonical: m.match,
});
}
}
function processOptionalSegment(
rest: string[],
idx: number,
validList: string[],
segment: string,
warnings: ValidationWarning[],
aliases: Record<string, string> = {},
): number {
if (idx >= rest.length) return idx;
const m = matchToken(rest, idx, validList, aliases);
if (!m.match) return idx;
addLegacyWarning(warnings, segment, m);
return idx + m.consumed;
}
export function validateComponentTokens(
tokens: string[],
scope: string,
expectedComponent: string | null,
meta: Metadata,
aliases: Record<string, string> = {},
): ValidationResult {
const errors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
const cv = meta.component.valid;
// Anti-pattern detection runs first: hooks following an RFC-rejected
// shape (property-first color, bare `-height`/`-width`, retired
// `-context-` infix) get a clear rename hint instead of a generic
// structural error from the segment walker.
const fullProp = `--${tokens.join('-')}`;
const antiPattern = detectAntiPattern(fullProp);
if (antiPattern) {
errors.push(antiPattern);
return { errors, warnings };
}
// Minimum 4 segments: `{ns}-{scope}-{component}-{category}`. Many
// RFC-canonical hooks stand alone at the category (`--slds-c-icon-size`,
// `--slds-c-button-shadow`, `--slds-c-badge-display`) and would
// wrongly fail a 5-segment minimum.
if (tokens.length < 4) {
errors.push(
err('structure', 'at least 4 segments (namespace-scope-component-category)', tokens.join('-')),
);
return { errors, warnings };
}
const component = tokens[2];
if (expectedComponent && component !== expectedComponent) {
errors.push(err('context', [expectedComponent], component));
}
const rest = tokens.slice(3);
const categoryIdx = findCategoryIndex(rest, cv.category);
if (categoryIdx === -1) {
errors.push(err('category', cv.category, rest.join('-') || '(missing)'));
return { errors, warnings };
}
const categoryToken = rest[categoryIdx];
let idx = categoryIdx + 1;
// Property is required for some categories (RFC 1157: `color`
// requires `-background` / `-foreground` / `-border`); optional for
// most others (`shadow`, `size`, `display`, `gap`, ... stand alone).
const propertyRequired = (cv.categoriesRequiringProperty ?? []).includes(categoryToken);
if (idx >= rest.length) {
if (propertyRequired) {
errors.push(err('property', cv.property, '(missing)'));
return { errors, warnings };
}
} else {
const propMatch = matchToken(rest, idx, cv.property, aliases);
if (propMatch.match) {
addLegacyWarning(warnings, 'property', propMatch);
idx += propMatch.consumed;
} else if (propertyRequired) {
errors.push(err('property', cv.property, rest[idx]));
return { errors, warnings };
}
// No match + not required: the segment may be an attribute / state
// / pseudoState (e.g. `shadow-focus` where `focus` is the state).
// Fall through; trailing-leftover check below catches anything else.
}
idx = processOptionalSegment(rest, idx, cv.attribute, 'attribute', warnings, aliases);
// Per-category state restriction (RFC 1157 §State Hooks): state
// suffixes are permitted only on categories listed in
// `categoriesAllowingState` (color-{background|foreground|border},
// shadow). When a state suffix appears under a different category,
// the parser would still consume the token; instead, we peek and
// emit a targeted error so the user gets a clear message.
const stateAllowed = (cv.categoriesAllowingState ?? []).includes(categoryToken);
const remainingToken = rest[idx];
if (
!stateAllowed &&
remainingToken !== undefined &&
(cv.state.includes(remainingToken) || cv.pseudoState.includes(remainingToken))
) {
errors.push(
err(
'state-not-allowed-on-category',
cv.categoriesAllowingState ?? [],
`${categoryToken}-...-${remainingToken}`,
),
);
return { errors, warnings };
}
idx = processOptionalSegment(rest, idx, cv.state, 'state', warnings);
idx = processOptionalSegment(rest, idx, cv.pseudoState, 'pseudoState', warnings);
if (idx < rest.length) {
errors.push(
err(
'structure',
'Valid segments after category are: property, attribute, state, pseudoState',
rest.slice(idx).join('-'),
),
);
}
return { errors, warnings };
}
export default validateComponentTokens;
|