Skip to content

Commit 789f31a

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix static CVA config and compound condition extraction
1 parent 4ec8f70 commit 789f31a

3 files changed

Lines changed: 298 additions & 69 deletions

File tree

‎scripts/check-design-conformance-command.test.ts‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,128 @@ test.each([
468468
}
469469
)
470470

471+
test.each([
472+
[
473+
'inline variant change',
474+
'',
475+
"{variants:{size:{small:'bg-red-500'}}}",
476+
"{variants:{size:{small:'bg-blue-500'}}}",
477+
true,
478+
false,
479+
],
480+
[
481+
'local const config',
482+
"const config={variants:{size:{small:'bg-red-500'}}} as const;",
483+
'config',
484+
'config',
485+
true,
486+
false,
487+
],
488+
[
489+
'local config alias',
490+
"const original={variants:{size:{small:'bg-red-500'}}};const config=(original as typeof original);",
491+
'config',
492+
'config',
493+
true,
494+
false,
495+
],
496+
[
497+
'compound literal array',
498+
'',
499+
"{compoundVariants:[{size:['small'],class:'rounded-lg'}]}",
500+
"{compoundVariants:[{size:['large'],class:'rounded-lg'}]}",
501+
true,
502+
false,
503+
],
504+
[
505+
'compound array alias',
506+
"const sizes=['small'] as const;",
507+
"{compoundVariants:[{'size':sizes,class:'rounded-lg'}]}",
508+
"{compoundVariants:[{'size':sizes,class:'rounded-lg'}]}",
509+
true,
510+
false,
511+
],
512+
[
513+
'compound array order',
514+
'',
515+
"{compoundVariants:[{size:['small','large'],class:'rounded-lg'}]}",
516+
"{compoundVariants:[{size:['large','small'],class:'rounded-lg'}]}",
517+
false,
518+
false,
519+
],
520+
['opaque configuration', '', 'loadConfig()', 'otherConfig()', false, true],
521+
[
522+
'mutated configuration',
523+
"const config={variants:{size:{small:'bg-red-500'}}};const escaped=(config as typeof config);escaped.variants.size.small=external;",
524+
'config',
525+
'config',
526+
false,
527+
true,
528+
],
529+
[
530+
'opaque compound condition',
531+
'',
532+
"{compoundVariants:[{size:[current],class:'rounded-lg'}]}",
533+
"{compoundVariants:[{size:[other],class:'rounded-lg'}]}",
534+
false,
535+
true,
536+
],
537+
[
538+
'compound array limit',
539+
'',
540+
`{compoundVariants:[{size:[${Array(65).fill("'small'").join(',')}],class:'rounded-lg'}]}`,
541+
`{compoundVariants:[{size:[${Array(65).fill("'large'").join(',')}],class:'rounded-lg'}]}`,
542+
false,
543+
true,
544+
],
545+
] as const)(
546+
'source-only CVA config and compound evidence survives the real CLI: %s',
547+
(name, declarations, beforeConfig, afterConfig, notify, unchecked) => {
548+
const { repo } = fixture()
549+
const sourceFile = 'packages/emcn/src/components/recipe.ts'
550+
const write = (file: string, source: string) => {
551+
mkdirSync(path.dirname(path.join(repo, file)), { recursive: true })
552+
writeFileSync(path.join(repo, file), source)
553+
}
554+
write('packages/emcn/src/index.ts', "export {recipe} from './components/recipe'")
555+
const source = (config: string, after: boolean) =>
556+
`import {cva} from 'class-variance-authority';${after ? declarations.replace('bg-red-500', 'bg-blue-500').replace("['small']", "['large']").replace('=external', '=other') : declarations}export const recipe=cva('p-2',${config.replace('{compoundVariants:', "{variants:{size:{small:'',large:''}},compoundVariants:")});throw new Error('Source must not execute')`
557+
write(sourceFile, source(beforeConfig, false))
558+
write(
559+
ui,
560+
"import {recipe} from '@sim/emcn';const A=()=> <div className={recipe({size:'small'})}/>"
561+
)
562+
const base = commit(repo)
563+
write(sourceFile, source(afterConfig, true))
564+
const child = run(['--repo', repo, '--base', base, '--working-tree', '--format', 'json'])
565+
expect([0, 1], child.stderr).toContain(child.status)
566+
const report = JSON.parse(child.stdout) as Report
567+
expect(report.status, child.stdout).toBe('completed')
568+
expect(
569+
report.findings.some(
570+
(finding) => finding.file === sourceFile && finding.rule === 'central-definition'
571+
),
572+
child.stdout
573+
).toBe(notify)
574+
expect(
575+
report.unchecked.some(
576+
(note) => note.file === sourceFile && /CVA.*unchecked|unchecked.*CVA/i.test(note.reason)
577+
),
578+
child.stdout
579+
).toBe(unchecked)
580+
if (name === 'compound literal array' || name === 'compound array alias')
581+
expect(
582+
report.findings.some(
583+
(finding) =>
584+
finding.file === sourceFile &&
585+
finding.context.includes('compound:') &&
586+
finding.context.includes('large')
587+
),
588+
child.stdout
589+
).toBe(true)
590+
}
591+
)
592+
471593
test.each([
472594
['grid line integer', 'gridColumnStart', 'grid-column-start', '1', 1, ''],
473595
['grid row integer', 'gridRowEnd', 'grid-row-end', '3', 3, ''],

‎scripts/design-conformance/control-static-inputs.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,23 @@ export const omitted: Expr = { kind: 'undefined' }
2121
const key = (n: t.Node) => (t.isIdentifier(n) ? n.name : t.isStringLiteral(n) ? n.value : '')
2222

2323
/** const protects the binding, not its object. Reject writes and unmodelled escapes. */
24-
export function immutable(binding: Binding, seen = new Set<Binding>()): boolean {
24+
export function immutable(
25+
binding: Binding,
26+
seen = new Set<Binding>(),
27+
readOnlyReference?: (reference: NodePath) => boolean
28+
): boolean {
2529
if (!binding.constant || seen.has(binding)) return false
2630
const next = new Set(seen).add(binding)
2731
return binding.referencePaths.every((p) => {
32+
if (readOnlyReference?.(p)) return true
2833
let q = p
29-
while (q.parentPath?.isMemberExpression() && q.parentPath.get('object') === q) q = q.parentPath
34+
while (
35+
(q.parentPath?.isMemberExpression() && q.parentPath.get('object') === q) ||
36+
q.parentPath?.isTSAsExpression() ||
37+
q.parentPath?.isTSSatisfiesExpression() ||
38+
q.parentPath?.isTSNonNullExpression()
39+
)
40+
q = q.parentPath
3041
const parent = q.parentPath
3142
if (!parent) return false
3243
if (parent.isObjectProperty()) {
@@ -66,7 +77,7 @@ export function immutable(binding: Binding, seen = new Set<Binding>()): boolean
6677
t.isIdentifier(parent.node.id)
6778
) {
6879
const alias = parent.scope.getBinding(parent.node.id.name)
69-
return !!alias && immutable(alias, next)
80+
return !!alias && immutable(alias, next, readOnlyReference)
7081
}
7182
return true
7283
})

0 commit comments

Comments
 (0)