fix: prevent false CSS class matches in AI guard
This commit is contained in:
1 parent
26d78763cc
commit
d6d6eb53af
3 files changed
+434
-10
No files matched your search
@@ -7,6 +7,9 @@ import { spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import test from 'node:test';
|
||||
import { parse } from '@babel/parser';
|
||||
import postcss from 'postcss';
|
||||
import { styleAnalysis } from './guard-style-analysis.mjs';
|
||||
|
||||
test('64. finite conditional templates and concatenation validate every Token branch', () => {
|
||||
fixture({ 'src/App.tsx': 'import { Button } from "@hm-design/ui"; const name = location.hash ? "fg-primary" : "bg-content"; export default () => <Button style={{ color: `var(--hm-sem-${name})` }}/>' }, root => {
|
||||
@@ -861,3 +864,161 @@ test('86. Component implementation classes and unrelated graph mutations stay pr
|
||||
assert.equal(r.styleEvidence.checks.find(c => c.selector.startsWith('.legacy')).applicability.classInputsComplete, false);
|
||||
});
|
||||
});
|
||||
|
||||
// Exercise implementation binding analysis without editing the real packages.
|
||||
function implementationFixture(source, callback, extra = {}) {
|
||||
fixture({
|
||||
'src/App.tsx': 'export default () => <Fixture/>;',
|
||||
'src/styles.css': '.legacy [data-slot="button"] { padding: 17px; }',
|
||||
'packages/ui/src/index.ts': 'export { Fixture } from "./fixture";',
|
||||
'packages/ui/src/fixture.tsx': source,
|
||||
...extra,
|
||||
}, root => {
|
||||
const parseJS = (text, file) => parse(text, { sourceType: 'unambiguous', plugins: [...(/\.[cm]?tsx?$/.test(file) ? ['typescript'] : []), 'jsx'] });
|
||||
const walk = (node, fn, parent = null) => {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
if (node.type) fn(node, parent);
|
||||
for (const [key, value] of Object.entries(node)) {
|
||||
if (['loc', 'start', 'end', 'comments', 'tokens', 'extra'].includes(key)) continue;
|
||||
if (Array.isArray(value)) value.forEach(x => walk(x, fn, node));
|
||||
else if (value && typeof value === 'object') walk(value, fn, node);
|
||||
}
|
||||
};
|
||||
const file = path.join(root, 'src/App.tsx'), ast = parseJS(fs.readFileSync(file, 'utf8'), file), renders = [];
|
||||
walk(ast, n => {
|
||||
if (n.type === 'JSXOpeningElement') renders.push({ name: n.name.name, attrs: new Map(n.attributes.filter(a => a.type === 'JSXAttribute').map(a => [a.name.name, a.value?.type === 'JSXExpressionContainer' ? a.value.expression : a.value])) });
|
||||
});
|
||||
const implementationFiles = dir => fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => e.isDirectory() ? implementationFiles(path.join(dir, e.name)) : [path.join(dir, e.name)]);
|
||||
const issues = [];
|
||||
const checks = styleAnalysis({ records: [{ file: path.join(root, 'src/styles.css'), css: true }], scopeRecords: [{ file, ast, renders }], target: root, postcss, walk, parseJS, selectedFile: () => true, knownSlot: s => s === 'button', origin: (_, name) => name === 'Fixture' ? { pkg: '@hm-design/ui', name } : null, add: (severity, code) => issues.push({ severity, code }), storybook: false, scopeIncomplete: false, entries: [path.join(root, 'index.html')], implementationFiles: implementationFiles(path.join(root, 'packages/ui/src')) });
|
||||
callback({ issues, checks });
|
||||
});
|
||||
}
|
||||
|
||||
test('87. implementation comments non-class strings and partial tokens never prove an ancestor', () => {
|
||||
for (const source of [
|
||||
'// legacy remains documented\nexport const Fixture = () => <div className="work"/>;',
|
||||
'const title = "legacy"; export const Fixture = () => <div title={title} className="work"/>;',
|
||||
'export const Fixture = () => <div className="legacyish pre-legacy legacy/post"/>;',
|
||||
'import { cva } from "class-variance-authority"; const unused = cva("legacy"); export const Fixture = () => <div className="work"/>;',
|
||||
'import { cva } from "class-variance-authority"; const variants = cva("work", {variants:{mode:{legacy:"work"}}, defaultVariants:{mode:"legacy"}}); export const Fixture = () => <div className={variants()}/>;',
|
||||
]) implementationFixture(source, ({ issues, checks }) => {
|
||||
assert.ok(!issues.some(i => i.code === 'STYLE-001'));
|
||||
assert.equal(checks[0].applicability.implementationClassPossible, false);
|
||||
assert.equal(checks[0].applicability.status, 'NOT APPLICABLE');
|
||||
assert.equal(checks[0].staticStatus, 'PASS');
|
||||
assert.equal(checks[0].runtimeStatus, 'NOT VERIFIED');
|
||||
assert.equal(checks[0].designIntentStatus, 'NOT VERIFIED');
|
||||
});
|
||||
});
|
||||
|
||||
test('88. implementation exact JSX factory joiner and variant class tokens remain protected', () => {
|
||||
for (const source of [
|
||||
'export const Fixture = () => <div className="work legacy"/>;',
|
||||
'export const Fixture = () => <div class="legacy"/>;',
|
||||
'import React from "react"; export const Fixture = () => React.createElement("div", { className: "legacy" });',
|
||||
'import { cn } from "cn"; export const Fixture = () => <div className={cn("work", enabled && "legacy")}/>;',
|
||||
'import cx from "clsx"; export const Fixture = () => <div className={cx(["work", {legacy:enabled}])}/>;',
|
||||
'import { cva as variants, type VariantProps } from "class-variance-authority"; import { cn } from "cn"; const styles = variants("work", {variants:{mode:{on:"legacy",off:"work"}}, compoundVariants:[{mode:"off",className:"legacy"}],defaultVariants:{mode:"off"}}); export const Fixture = ({className,mode}: VariantProps<typeof styles>) => <div className={cn(styles({mode,className}))}/>;',
|
||||
'import { cva } from "class-variance-authority"; const styles = cva("work", {compoundVariants:[{class:"legacy"}]}); export const Fixture = () => <div className={styles()}/>;',
|
||||
'export const Fixture = ({className="legacy"}) => <div className={className}/>;',
|
||||
]) implementationFixture(source, ({ issues, checks }) => {
|
||||
assert.ok(issues.some(i => i.code === 'STYLE-001' && i.severity === 'FAIL'));
|
||||
assert.equal(checks[0].applicability.implementationClassPossible, true);
|
||||
assert.equal(checks[0].staticStatus, 'FAIL');
|
||||
});
|
||||
});
|
||||
|
||||
test('89. imported implementation constants variants aliases and barrels retain class evidence', () => {
|
||||
for (const [source, helper] of [
|
||||
['import { name as applied } from "./barrel"; export const Fixture = () => <div className={applied}/>;', 'export const name = "legacy";'],
|
||||
['import { styles } from "./barrel"; export const Fixture = () => <div className={styles()}/>;', 'import { cva } from "class-variance-authority"; export const styles = cva("legacy");'],
|
||||
['import { name } from "./nested"; export const Fixture = () => <div className={name}/>;', 'export const name = "legacy";'],
|
||||
]) implementationFixture(source, ({ issues, checks }) => {
|
||||
assert.ok(issues.some(i => i.code === 'STYLE-001'));
|
||||
assert.equal(checks[0].applicability.implementationClassPossible, true);
|
||||
}, { 'packages/ui/src/barrel.ts': 'export * from "./helper";', 'packages/ui/src/nested/index.ts': 'export { name } from "../helper.js";', 'packages/ui/src/helper.ts': helper });
|
||||
});
|
||||
|
||||
test('90. unresolved connected implementation inputs cannot prove scope exclusion', () => {
|
||||
for (const source of [
|
||||
'export const Fixture = ({value}) => <div className={value}/>;',
|
||||
'export const Fixture = () => <div className={computeClass()}/>;',
|
||||
'function cn(){return location.hash;} export const Fixture = () => <div className={cn()}/>;',
|
||||
'import { cn } from "cn"; export const Fixture = ({cn}) => <div className={cn()}/>;',
|
||||
'import { cn } from "cn"; export function Fixture(){ function cn(){return location.hash;} return <div className={cn()}/>; }',
|
||||
'import { cn } from "cn"; export function Fixture(){ try { throw makeClass(); } catch(cn){ return <div className={cn()}/>; } }',
|
||||
'export const Fixture = ({value}) => <div {...value}/>;',
|
||||
'export const Fixture = ({props}) => <div className={props.className}/>;',
|
||||
'export const Fixture = ({legacy:className}) => <div className={className}/>;',
|
||||
'export const Fixture = (props=getProps()) => <div className={props.className}/>;',
|
||||
'const Other=({className}) => <div className={className}/>; export const Fixture = ({legacy:className}) => <div className={className}/>;',
|
||||
'const Other=(props) => <div className={props.className}/>; export const Fixture = ({props}) => <div className={props.className}/>;',
|
||||
'export const Fixture = () => <div className={`prefix-${location.hash}`}/>;',
|
||||
'const opaque = getProps(); export const Fixture = () => <div {...opaque}/>;',
|
||||
'document.body.classList.add(location.hash); export const Fixture = () => <div/>;',
|
||||
'import "./mutation"; export const Fixture = () => <div/>;',
|
||||
'import("./mutation"); export const Fixture = () => <div/>;',
|
||||
'import { value } from "./missing"; export const Fixture = () => <div className={value}/>;',
|
||||
'export const Fixture = () => <div',
|
||||
]) implementationFixture(source, ({ issues, checks }) => {
|
||||
assert.ok(issues.some(i => i.code === 'STYLE-001'));
|
||||
assert.equal(checks[0].applicability.status, 'POSSIBLE');
|
||||
assert.equal(checks[0].applicability.classInputsComplete, false);
|
||||
assert.ok(checks[0].applicability.unresolvedImplementationFiles.length);
|
||||
}, { 'packages/ui/src/mutation.ts': 'document.body.className = location.hash;' });
|
||||
});
|
||||
|
||||
test('91. missing ancestors retain CSS inventory and unverified intent in the real Guard', () => {
|
||||
for (const ancestor of ['legacy', 'hm_unused_screen_audit', 'hm_guard_absent_9e4b']) fixture({
|
||||
'src/App.tsx': 'import { Button } from "@hm-design/ui"; const title="legacy"; export default () => <div title={title} className="legacyish"><Button/></div>;',
|
||||
'src/styles.css': '@import "./tokens.css"; /* legacy */ .'+ancestor+' [data-slot="button"] { padding:17px; }',
|
||||
}, root => {
|
||||
const r = run(root, ['--scope', 'FULL_SCREEN']).report;
|
||||
assert.ok(!r.issues.some(i => i.code === 'STYLE-001'));
|
||||
const c = r.styleEvidence.checks.find(c => c.component === 'button');
|
||||
assert.equal(c.applicability.status, 'NOT APPLICABLE');
|
||||
assert.equal(r.styleEvidence.staticStatus, 'PASS');
|
||||
assert.equal(r.styleEvidence.finalStatus, 'NOT VERIFIED');
|
||||
assert.equal(r.status, 'NOT VERIFIED');
|
||||
assert.ok(r.issues.some(i => i.code === 'REUSE-003' && i.severity === 'NOT VERIFIED'));
|
||||
});
|
||||
});
|
||||
|
||||
test('92. unique dynamic ancestor controls cannot pass because of library substring coincidences', () => {
|
||||
const ancestor = 'hm_guard_dynamic_9e4b';
|
||||
for (const app of [
|
||||
'const name=location.hash?"'+ancestor+'":"work"; export default () => <div className={name}><Button/></div>;',
|
||||
'export default ({className}) => <div className={className}><Button/></div>;',
|
||||
'export default ({props}) => <div {...props}><Button/></div>;',
|
||||
'document.body["classList"].add(location.hash); export default () => <Button/>;',
|
||||
'import "./mutation"; export default () => <Button/>;',
|
||||
'function cn(){return location.hash;} export default () => <div className={cn()}><Button/></div>;',
|
||||
'import { cn } from "cn"; export default ({cn}) => <div className={cn()}><Button/></div>;',
|
||||
'import { cn } from "cn"; export default function App(){function cn(){return location.hash;} return <div className={cn()}><Button/></div>}',
|
||||
]) fixture({ 'src/App.tsx': 'import { Button } from "@hm-design/ui"; '+app, 'src/mutation.ts': 'document.body.className = location.hash;', 'src/styles.css': '@import "./tokens.css"; .'+ancestor+' [data-slot="button"] { padding:17px; }' }, root => {
|
||||
const r = run(root, ['--scope', 'FULL_SCREEN']).report;
|
||||
assert.ok(r.issues.some(i => i.code === 'STYLE-001' && i.severity === 'FAIL'));
|
||||
assert.equal(r.styleEvidence.checks.find(c => c.component === 'button').applicability.implementationClassPossible, false);
|
||||
});
|
||||
});
|
||||
|
||||
test('93. actual internal flex and public variant classes remain exact tokens', () => {
|
||||
for (const ancestor of ['flex', 'inline-flex', 'group', 'h-8']) fixture({ 'src/App.tsx': 'import { Button } from "@hm-design/ui"; export default () => <Button/>;', 'src/styles.css': '@import "./tokens.css"; .'+ancestor+' [data-slot="button"] { padding:17px; }' }, root => {
|
||||
const r = run(root, ['--scope', 'FULL_SCREEN']).report;
|
||||
assert.ok(r.issues.some(i => i.code === 'STYLE-001'));
|
||||
assert.equal(r.styleEvidence.checks.find(c => c.component === 'button').applicability.implementationClassPossible, true);
|
||||
});
|
||||
});
|
||||
|
||||
test('94. consumer class joiners match exact tokens and preserve unknown-class review', () => {
|
||||
for (const present of [false, true]) fixture({
|
||||
'src/App.tsx': 'import { Button } from "@hm-design/ui"; import { clsx as cx } from "clsx"; export default () => <div className={cx("work", enabled && "'+(present ? 'legacy' : 'legacyish')+'")}><Button/></div>;',
|
||||
'src/styles.css': '@import "./tokens.css"; .legacy [data-slot="button"] { padding:17px; }',
|
||||
}, root => {
|
||||
const r = run(root, ['--scope', 'FULL_SCREEN']).report;
|
||||
assert.equal(r.issues.some(i => i.code === 'STYLE-001'), present);
|
||||
assert.equal(r.styleEvidence.checks.find(c => c.component === 'button').applicability.status, present ? 'POSSIBLE' : 'NOT APPLICABLE');
|
||||
assert.ok(r.issues.some(i => i.code === 'TOKEN-002' && i.severity === 'NOT VERIFIED'));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user