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 | 17x 196x 132x 135x 135x 135x 144x 144x 48x 135x 87x 73x 73x 73x 152x 71x 301x 301x 228x 17x 246x 246x 314x | /**
* Shared internals for the structural Rule checks.
*
* Every structural rule walks the same shape: each component-source CSS
* file's flattened PostCSS root, every rule that contains at least one
* declaration whose property starts with `--slds-c-`, then a per-rule
* judgment based on the selector / layer / sibling decls / pseudo-state
* status. The helpers here factor out the AST walking and offender
* collection so each rule file can stay focused on its judgment.
*/
import type { Rule, Declaration } from 'postcss';
import type {
ComplianceCategory,
ComplianceRow,
ComponentCheckInput,
ComponentSourceFile,
ComponentSourceFileRole,
Offender,
} from '../../types.js';
const HOOK_PROP_RE = /^--slds-c-[a-z0-9-]+$/i;
/** Returns true when a declaration writes a customer hook. */
export function isHookWriteDecl(decl: Declaration): boolean {
return HOOK_PROP_RE.test(decl.prop);
}
/** Visit every rule in a source file that writes at least one customer hook. */
export function visitHookWriteRules(
file: ComponentSourceFile,
visit: (ctx: { rule: Rule; hookDecls: Declaration[]; nonHookDecls: Declaration[] }) => void,
): void {
file.root.walkRules((rule) => {
const hookDecls: Declaration[] = [];
const nonHookDecls: Declaration[] = [];
rule.walkDecls((decl) => {
// Only direct children of this rule, so we don't accidentally
// double-count declarations that belong to a nested rule.
Iif (decl.parent !== rule) return;
if (isHookWriteDecl(decl)) hookDecls.push(decl);
else nonHookDecls.push(decl);
});
if (hookDecls.length === 0) return;
visit({ rule, hookDecls, nonHookDecls });
});
}
/**
* Build a relative file:line reference for a declaration. The file
* path is already relative (the CLI sets it); the line comes from
* PostCSS source positions, which are present whenever the file was
* loaded from disk.
*/
export function declLocation(file: ComponentSourceFile, decl: Declaration): string {
const line = decl.source?.start?.line;
return line ? `${file.path}:${line}` : file.path;
}
/**
* Each structural check belongs to a sub-group: `customer-reach`
* (override-blocking, fails are `fail`) or `cascade-hygiene`
* (system-surface integrity, fails are `review`). The check passes
* its category once at the top and threads it through every row it
* emits.
*/
export type StructuralCategory = Extract<ComplianceCategory, 'customer-reach' | 'cascade-hygiene'>;
/** Standard "no source files supplied" row. */
export function notRunYetRow(id: string, label: string, category?: StructuralCategory): ComplianceRow {
return {
id,
label,
category,
status: 'info',
count: 0,
detail:
'Component source files were not supplied to this check (browser / themeData-only run). Run `sds-compliance build` to populate the structural rows.',
};
}
/** Standard "passed" row builder used by every structural rule. */
export function passRow(
id: string,
label: string,
detail: string,
category?: StructuralCategory,
): ComplianceRow {
return { id, label, category, status: 'pass', count: 0, detail };
}
/**
* Standard "failed" row for `customer-reach` checks (the customer's
* override is blocked; severity is `fail`).
*/
export function failRow(id: string, label: string, detail: string, offenders: Offender[]): ComplianceRow {
return {
id,
label,
category: 'customer-reach',
status: 'fail',
count: offenders.length,
detail,
offenders,
};
}
/**
* Standard "needs review" row for `cascade-hygiene` checks. Severity
* is `review` because the system surface can leak via misuse, but
* correctly-rendered components are unaffected — needs eyes, not
* blocking.
*/
export function reviewRow(id: string, label: string, detail: string, offenders: Offender[]): ComplianceRow {
return {
id,
label,
category: 'cascade-hygiene',
status: 'review',
count: offenders.length,
detail,
offenders,
};
}
/** Convenience: pull source files off the input, or null if absent. */
export function sourceFilesFor(input: ComponentCheckInput): ComponentSourceFile[] | null {
const files = input.componentSourceFiles;
if (!files || files.length === 0) return null;
return files;
}
const THEME_LAYER_ROLES: ReadonlySet<ComponentSourceFileRole> = new Set<ComponentSourceFileRole>([
'theme',
'theme-base',
]);
/**
* Convenience: pull only the *theme layer* source files off the input
* (`role: 'theme' | 'theme-base'`), or `null` if none were supplied.
*
* The three authoring rules from RFC 1157 (`no descendant`, `no BEM
* element`, `no attribute substring`) plus `no pseudo-state hook writes`
* and `no :root component hooks` only apply to theme layer files. Legacy `<component>.css` files (role:
* `'base'`) and auxiliary CSS (role: `'aux'`) are out of scope; they
* are the existing paint surface SLDS already ships, not the
* customer-facing theme layer the compliance package gates.
*
* Returning `null` when no source files were supplied at all preserves
* the existing "not run yet" behavior (callers translate it via
* {@link notRunYetRow}). Returning an empty array when source files
* were supplied but none are theme layer is the caller's signal to
* pass trivially with a clear "no theme files" message.
*/
export function themeLayerFilesFor(input: ComponentCheckInput): ComponentSourceFile[] | null {
const files = sourceFilesFor(input);
if (!files) return null;
return files.filter((file) => THEME_LAYER_ROLES.has(file.role));
}
|