import fs from 'node:fs'; import path from 'node:path'; import { staticStrings, localConstants } from './guard-theme-analysis.mjs'; export const verdict = values => values.includes('FAIL') ? 'FAIL' : values.includes('NOT VERIFIED') ? 'NOT VERIFIED' : 'PASS'; const lineOf = n => n?.loc?.start?.line ?? n?.source?.start?.line ?? 1; const rawColor = /#[\da-f]{3,8}\b|\b(?:rgb|hsl)a?\(/i; const geometry = /^(?:padding|margin|gap|row-gap|column-gap|border-(?:(?:top|bottom)-(?:left|right)-)?radius)(?:-|$)/; function withoutVariables(value) { let result = ''; for (let i = 0; i < value.length; i++) { if (value.slice(i, i + 4) !== 'var(') { result += value[i]; continue; } let depth = 1; i += 3; while (++i < value.length && depth) { if (value[i] === '(') depth++; else if (value[i] === ')') depth--; } i--; } return result; } function knownStyleStrings(node, constants, seen = new Set()) { const finite = staticStrings(node, constants); if (finite) return finite; if (node?.type === 'ConditionalExpression') return [...knownStyleStrings(node.consequent, constants, seen), ...knownStyleStrings(node.alternate, constants, seen)]; if (node?.type === 'TemplateLiteral') return node.quasis.map(q => q.value.cooked ?? q.value.raw); if (node?.type === 'Identifier' && constants.has(node.name) && !seen.has(node.name)) return knownStyleStrings(constants.get(node.name), constants, new Set([...seen, node.name])); return []; } const propertyName = n => n?.computed ? n.property?.value : n?.property?.name; const nodeName = n => n?.name ?? n?.value; const classKeys = new Set(['className', 'class']); const joiners = new Set(['cn', 'clsx', 'classNames', 'twMerge']); const helperPackages = new Set(['cn', 'clsx', 'classnames', 'tailwind-merge', 'class-variance-authority']); const helperName = (context, callee) => { if (context.shadowed?.has(callee.name)) return null; const imported = context.imports?.get(callee.name); return imported && helperPackages.has(imported.source) ? imported.name === 'default' ? imported.source === 'classnames' ? 'classNames' : imported.source : imported.name : null; }; const mergeCandidates = results => ({ values: results.flatMap(r => r.values), unknown: results.some(r => r.unknown) }); const unknownClasses = () => ({ values: [], unknown: true }); const functionNodes = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression', 'ObjectMethod', 'ClassMethod', 'ClassPrivateMethod']); function bindingNames(node) { if (!node) return []; if (node.type === 'Identifier') return [node.name]; if (node.type === 'RestElement') return bindingNames(node.argument); if (node.type === 'AssignmentPattern') return bindingNames(node.left); if (node.type === 'ArrayPattern') return node.elements.flatMap(bindingNames); if (node.type === 'ObjectPattern') return node.properties.flatMap(p => bindingNames(p.type === 'RestElement' ? p.argument : p.value)); return []; } // Only expressions feeding a class binding are evidence. Never search comments, // arbitrary strings, CSS selectors or variant names for a substring match. function classCandidates(node, context, resolveBinding, internal = false, seen = new Set()) { if (!node) return unknownClasses(); const finite = staticStrings(node, context.constants); if (finite) return { values: finite, unknown: false }; const recurse = n => classCandidates(n, context, resolveBinding, internal, seen); if (['JSXExpressionContainer', 'TSAsExpression', 'TSNonNullExpression', 'ParenthesizedExpression'].includes(node.type)) return recurse(node.expression); if (['BooleanLiteral', 'NullLiteral'].includes(node.type)) return { values: [], unknown: false }; if (node.type === 'ConditionalExpression') return mergeCandidates([recurse(node.consequent), recurse(node.alternate)]); if (node.type === 'LogicalExpression') return node.operator === '&&' ? recurse(node.right) : mergeCandidates([recurse(node.left), recurse(node.right)]); if (node.type === 'ArrayExpression') return mergeCandidates(node.elements.filter(Boolean).map(recurse)); if (node.type === 'ObjectExpression') return mergeCandidates(node.properties.map(p => { if (p.type !== 'ObjectProperty') return unknownClasses(); if (p.value.type === 'BooleanLiteral' && !p.value.value || p.value.type === 'NullLiteral') return { values: [], unknown: false }; return p.computed ? recurse(p.key) : { values: [String(nodeName(p.key))], unknown: false }; })); if (node.type === 'Identifier') { const key = `${context.file}:${node.name}`; if (seen.has(key)) return unknownClasses(); const binding = resolveBinding(context, node.name); if (binding?.node) return classCandidates(binding.node, binding.context, resolveBinding, internal, new Set([...seen, key])); // Public className forwarding is already checked at consumer call sites. if (internal && context.forwardedClasses.has(node.name) && !context.declared.has(node.name)) return { values: [], unknown: false }; return unknownClasses(); } if (node.type === 'MemberExpression' && internal && classKeys.has(propertyName(node)) && context.forwardedProps.has(node.object?.name) && !context.declared.has(node.object.name)) return { values: [], unknown: false }; if (node.type === 'CallExpression') { const name = node.callee.name ?? propertyName(node.callee); const helper = helperName(context, node.callee); if (joiners.has(helper)) return mergeCandidates(node.arguments.map(recurse)); if (helper === 'cva') { const config = node.arguments[1]; const results = node.arguments[0] ? [recurse(node.arguments[0])] : []; if (config && config.type !== 'ObjectExpression') results.push(unknownClasses()); for (const p of config?.properties ?? []) { if (p.type !== 'ObjectProperty' || p.computed) { results.push(unknownClasses()); continue; } if (nodeName(p.key) === 'variants') { if (p.value.type !== 'ObjectExpression') { results.push(unknownClasses()); continue; } for (const variant of p.value.properties) { if (variant.type !== 'ObjectProperty' || variant.value.type !== 'ObjectExpression') { results.push(unknownClasses()); continue; } results.push(...variant.value.properties.map(option => option.type === 'ObjectProperty' ? recurse(option.value) : unknownClasses())); } } if (nodeName(p.key) === 'compoundVariants') { if (p.value.type !== 'ArrayExpression') { results.push(unknownClasses()); continue; } for (const item of p.value.elements) { if (item?.type !== 'ObjectExpression') { results.push(unknownClasses()); continue; } for (const field of item.properties) { if (field.type !== 'ObjectProperty' || field.computed) results.push(unknownClasses()); else if (classKeys.has(nodeName(field.key))) results.push(recurse(field.value)); } } } } return mergeCandidates(results); } const binding = node.callee.type === 'Identifier' ? resolveBinding(context, node.callee.name) : null; if (binding?.node?.type === 'CallExpression' && helperName(binding.context, binding.node.callee) === 'cva') { const results = [classCandidates(binding.node, binding.context, resolveBinding, internal, seen)]; for (const arg of node.arguments) { if (arg.type !== 'ObjectExpression') { results.push(unknownClasses()); continue; } for (const p of arg.properties) { if (p.type !== 'ObjectProperty' || p.computed) results.push(unknownClasses()); else if (classKeys.has(nodeName(p.key))) results.push(recurse(p.value)); } } return mergeCandidates(results); } if (name === 'trim') return recurse(node.callee.object); if (name === 'join' && node.arguments[0]?.value === ' ') return recurse(node.callee.object); // An unrelated function named cn/clsx is opaque, not a proven joiner. if (joiners.has(name)) return mergeCandidates([...node.arguments.map(recurse), unknownClasses()]); } return unknownClasses(); } function implementationClasses(files, parseJS, walk, usedComponents) { const classes = new Set(), modules = new Map(), fileSet = new Set(files.map(f => path.resolve(f))); const resolveFile = (file, spec) => { if (!spec?.startsWith('.')) return null; const base = path.resolve(path.dirname(file), spec); return [base, ...['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx'].map(ext => base + ext), ...(/\.[cm]?js$/.test(base) ? [base.replace(/\.[cm]?js$/, '.ts'), base.replace(/\.[cm]?js$/, '.tsx')] : [])].map(f => path.normalize(f)).find(f => fileSet.has(f)); }; for (const file of fileSet) { if (!/\.[cm]?[jt]sx?$/.test(file)) continue; const context = { file, constants: new Map(), imports: new Map(), exports: new Map(), stars: [], dependencies: new Set(), forwardedClasses: new Set(), forwardedProps: new Set(), declared: new Set(), shadowed: new Set(), parents: new WeakMap(), scopes: new WeakMap(), unknown: false }; modules.set(file, context); try { context.ast = parseJS(fs.readFileSync(file, 'utf8'), file); } catch { context.unknown = true; continue; } // Parameter type references (VariantProps) are not // bindings and must not shadow the immutable class factory they describe. for (const stmt of context.ast.program.body) { const declaration = stmt.type === 'ExportNamedDeclaration' ? stmt.declaration : stmt; if (declaration?.type === 'VariableDeclaration' && declaration.kind === 'const') for (const d of declaration.declarations) if (d.id.type === 'Identifier') context.constants.set(d.id.name, d.init); } for (const stmt of context.ast.program.body) { if (stmt.type === 'ImportDeclaration' && stmt.importKind !== 'type') { const dependency = resolveFile(file, stmt.source.value); if (stmt.source.value.startsWith('.') && !dependency) context.unknown = true; if (dependency) context.dependencies.add(dependency); for (const s of stmt.specifiers.filter(s => s.importKind !== 'type')) context.imports.set(s.local.name, { file: dependency, source: stmt.source.value, name: s.type === 'ImportSpecifier' ? nodeName(s.imported) : s.type === 'ImportDefaultSpecifier' ? 'default' : '*' }); } if (stmt.type === 'ExportAllDeclaration' && stmt.exportKind !== 'type') { const dependency = resolveFile(file, stmt.source.value); context.stars.push(dependency); if (dependency) context.dependencies.add(dependency); else context.unknown = true; } if (stmt.type === 'ExportNamedDeclaration' && stmt.exportKind !== 'type') { if (stmt.source) { const dependency = resolveFile(file, stmt.source.value); if (dependency) context.dependencies.add(dependency); else if (stmt.source.value.startsWith('.')) context.unknown = true; } for (const s of stmt.specifiers) context.exports.set(nodeName(s.exported), { file: stmt.source ? resolveFile(file, stmt.source.value) : file, name: nodeName(s.local) }); if (stmt.declaration?.id) context.exports.set(stmt.declaration.id.name, { file, name: stmt.declaration.id.name }); for (const d of stmt.declaration?.declarations ?? []) if (d.id.type === 'Identifier') context.exports.set(d.id.name, { file, name: d.id.name }); } if (stmt.type === 'ExportDefaultDeclaration') context.exports.set('default', { file, name: stmt.declaration.id?.name ?? stmt.declaration.name ?? 'default' }); } walk(context.ast, (n, parent) => { if (parent) context.parents.set(n, parent); if (n.type === 'VariableDeclarator' || functionNodes.has(n.type) && n.id) for (const name of bindingNames(n.id)) { context.declared.add(name); if (context.imports.has(name)) context.shadowed.add(name); if (context.constants.get(name) !== n.init) context.constants.delete(name); } const params = functionNodes.has(n.type) ? n.params : n.type === 'CatchClause' ? [n.param] : []; for (const p of params) for (const name of bindingNames(p)) { if (context.imports.has(name)) context.shadowed.add(name); context.constants.delete(name); } }); } const bindingContext = (context, node) => { let owner = node; while (owner && !functionNodes.has(owner.type)) owner = context.parents.get(owner); if (!owner) return context; if (context.scopes.has(owner)) return context.scopes.get(owner); const scope = { ...context, forwardedClasses: new Set(), forwardedProps: new Set() }; for (const p of owner.params) { if (p.type === 'Identifier') scope.forwardedProps.add(p.name); if (p.type === 'ObjectPattern') for (const field of p.properties) { if (field.type === 'RestElement' && field.argument.type === 'Identifier') scope.forwardedProps.add(field.argument.name); if (field.type !== 'ObjectProperty' || field.computed || !classKeys.has(nodeName(field.key))) continue; const binding = field.value.type === 'AssignmentPattern' ? field.value.left : field.value; if (binding.type === 'Identifier' && binding.name === nodeName(field.key)) scope.forwardedClasses.add(binding.name); } } context.scopes.set(owner, scope); return scope; }; const exported = (file, name, seen = new Set()) => { const key = `${file}:${name}`, context = modules.get(file); if (!context || seen.has(key)) return null; const next = new Set([...seen, key]), binding = context.exports.get(name); if (binding) return binding.file === file ? { context, name: binding.name } : exported(binding.file, binding.name, next); for (const star of context.stars) { const result = exported(star, name, next); if (result) return result; } return null; }; const resolveBinding = (context, name) => { if (context.constants.has(name)) return { context, node: context.constants.get(name) }; const imported = context.imports.get(name), binding = imported?.file && exported(imported.file, imported.name); return binding ? resolveBindingLocal(binding.context, binding.name, new Set()) : null; }; function resolveBindingLocal(context, name, seen) { const key = `${context.file}:${name}`; if (seen.has(key)) return null; seen.add(key); if (context.constants.has(name)) return { context, node: context.constants.get(name) }; const imported = context.imports.get(name), binding = imported?.file && exported(imported.file, imported.name); return binding ? resolveBindingLocal(binding.context, binding.name, seen) : null; } for (const context of modules.values()) if (context.ast) walk(context.ast, n => { const scope = bindingContext(context, n); let value; if (n.type === 'JSXAttribute' && classKeys.has(n.name.name)) value = n.value; if (n.type === 'CallExpression' && /^(?:createElement|_?jsx|_?jsxs|jsxDEV)$/.test(n.callee.name ?? propertyName(n.callee) ?? '')) for (const p of n.arguments[1]?.properties ?? []) if (p.type === 'ObjectProperty' && !p.computed && classKeys.has(nodeName(p.key))) collect(p.value); if (n.type === 'AssignmentPattern' && classKeys.has(n.left.name)) value = n.right; if (n.type === 'JSXAttribute' && n.name.name === 'dangerouslySetInnerHTML') context.unknown = true; if (n.type === 'JSXSpreadAttribute' && !(scope.forwardedProps.has(n.argument.name) && !context.declared.has(n.argument.name))) context.unknown = true; if (n.type === 'CallExpression') { const method = propertyName(n.callee); if (['setAttribute', 'setAttributeNS', 'insertAdjacentHTML', 'write', 'writeln'].includes(method) || propertyName(n.callee?.object) === 'classList') context.unknown = true; if (/^(?:createElement|_?jsx|_?jsxs|jsxDEV)$/.test(n.callee.name ?? method ?? '') && n.arguments[1] && (n.arguments[1].type !== 'ObjectExpression' || n.arguments[1].properties.some(p => p.type === 'SpreadElement' && !(scope.forwardedProps.has(p.argument.name) && !context.declared.has(p.argument.name))))) context.unknown = true; } if (n.type === 'ImportExpression' || n.type === 'CallExpression' && (n.callee.type === 'Import' || n.callee.name === 'require')) { const spec = (n.source ?? n.arguments[0])?.value; const dependency = typeof spec === 'string' ? resolveFile(context.file, spec) : null; if (dependency) context.dependencies.add(dependency); else context.unknown = true; } if (n.type === 'AssignmentExpression' && ['className', 'innerHTML', 'outerHTML'].includes(propertyName(n.left))) context.unknown = true; if (value) collect(value); function collect(expr) { const result = classCandidates(expr, bindingContext(context, expr), resolveBinding, true); for (const v of result.values) for (const token of v.split(/\s+/).filter(Boolean)) classes.add(token); context.unknown ||= result.unknown; } }); // Preserve the conservative public implementation class catalogue. Unknown // generated classes only block exclusions for connected implementation modules. const connected = new Set(); const connect = file => { if (!file || connected.has(file)) return; connected.add(file); for (const dependency of modules.get(file)?.dependencies ?? []) connect(dependency); }; for (const identity of usedComponents) { const packageDir = identity.pkg === '@hm-design/ui' ? 'ui' : identity.pkg === '@hm-design/center-graphic' ? 'center-graphic' : null; if (!packageDir) continue; const entry = [...modules.keys()].find(f => f.replaceAll('\\', '/').endsWith(`/packages/${packageDir}/src/index.ts`)); const binding = exported(entry, identity.name); if (binding) connect(binding.context.file); else if (entry) { // A missing implementation export/parse cannot prove class absence. modules.get(entry).unknown = true; connect(entry); } } const unresolvedFiles = [...connected].filter(file => !modules.has(file) || modules.get(file).unknown); return { classes, incomplete: unresolvedFiles.length > 0, unresolvedFiles }; } // A class prefix is not a public Component identity. Only explicit consumer // class bindings prove it. A simple positive ancestor absent from the complete // entry can bound a rule; dynamic classes/spreads/DOM writes retain strict checks. function classScope(records, entries, walk, origin, incomplete, implementationFiles, parseJS) { const classes = new Set(), componentClasses = new Map(); let uncertain = incomplete; const usedComponents = []; const memberName = n => n?.computed ? n.property?.value : n?.property?.name; const collect = (values, component) => { if (!values) { uncertain = true; return; } for (const value of values) for (const name of value.split(/\s+/).filter(Boolean)) { classes.add(name); if (component) componentClasses.set(name, component); } }; for (const r of records.filter(r => r.ast)) { const constants = localConstants(r.ast, walk); const imports = new Map(); for (const stmt of r.ast.program.body) if (stmt.type === 'ImportDeclaration' && stmt.importKind !== 'type') for (const s of stmt.specifiers.filter(s => s.importKind !== 'type')) imports.set(s.local.name, { source: stmt.source.value, name: s.type === 'ImportSpecifier' ? nodeName(s.imported) : s.type === 'ImportDefaultSpecifier' ? 'default' : '*' }); const shadowed = new Set(); walk(r.ast, n => { const bindings = n.type === 'VariableDeclarator' ? [n.id] : functionNodes.has(n.type) ? [...n.params, n.id] : n.type === 'CatchClause' ? [n.param] : []; for (const binding of bindings) for (const name of bindingNames(binding)) { if (imports.has(name)) shadowed.add(name); if (n.type !== 'VariableDeclarator') constants.delete(name); } }); for (const render of r.renders) { const identity = origin(r.file, render.name); if (identity?.pkg) usedComponents.push(identity); for (const key of ['className', 'class']) if (render.attrs.has(key)) { const result = classCandidates(render.attrs.get(key), { file: r.file, constants, imports, shadowed }, (context, name) => context.constants.has(name) ? { context, node: context.constants.get(name) } : null); collect(result.values, identity?.pkg === '@hm-design/ui' ? identity.name : null); uncertain ||= result.unknown; } } walk(r.ast, n => { if (n.type === 'JSXSpreadAttribute' || n.type === 'JSXAttribute' && n.name.name === 'dangerouslySetInnerHTML') uncertain = true; if (n.type === 'CallExpression') { const method = memberName(n.callee); if (['setAttribute', 'setAttributeNS', 'insertAdjacentHTML', 'write', 'writeln'].includes(method) || memberName(n.callee?.object) === 'classList') uncertain = true; if (/createElement|^(?:_?jsx|_?jsxs|jsxDEV)$/.test(n.callee?.name ?? method ?? '') && n.arguments[1] && (n.arguments[1].type !== 'ObjectExpression' || n.arguments[1].properties.some(p => p.type === 'SpreadElement'))) uncertain = true; } if (n.type === 'AssignmentExpression' && ['className', 'innerHTML', 'outerHTML'].includes(memberName(n.left))) uncertain = true; }); } const implementation = implementationClasses(implementationFiles, parseJS, walk, usedComponents); uncertain ||= implementation.incomplete; for (const file of entries.filter(f => f.endsWith('.html'))) { try { const markup = fs.readFileSync(file, 'utf8').replace(//g, '').replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, ''); for (const [, value] of markup.matchAll(/\bclass\s*=\s*["']([^"']*)["']/g)) collect([value]); if (/\bclass\s*=\s*[^"'\s]/.test(markup)) uncertain = true; } catch { uncertain = true; } } return { component(name) { return componentClasses.get(name) ?? (uncertain && name.startsWith('hm-') ? name.slice(3) : null); }, evidence(selector) { // Restrict the proof to a plain leading positive class and an explicit // descendant slot. Commas, pseudos, escapes and nesting stay unpruned. const plain = selector.replace(/\[[^\]]*\]/g, '[]'); const ancestor = !/[,:\\&]/.test(plain) && selector.match(/^\.([A-Za-z_][\w-]*)\s+/)?.[1]; const explicitSlot = /\[data-slot\s*=\s*(?:"[\w-]+"|'[\w-]+'|[\w-]+)\]/.test(selector); const implementationClassPossible = Boolean(ancestor && implementation.classes.has(ancestor)); const absent = ancestor && explicitSlot && !uncertain && !classes.has(ancestor) && !implementationClassPossible; return { status: absent ? 'NOT APPLICABLE' : 'POSSIBLE', requiredAncestorClass: ancestor || null, implementationClassPossible, classInputsComplete: !uncertain, unresolvedImplementationFiles: implementation.unresolvedFiles.map(f => f.replaceAll('\\', '/').split('/packages/').at(-1)), reason: absent ? 'Required positive ancestor class is absent from this complete entry graph, HTML and public implementation class bindings; CSS retained in inventory, not approved' : 'No proven scope exclusion; Component override policy remains enforced' }; }, }; } // Inventory consumer declarations. General layout is not a Component replica. export function styleAnalysis({ records, target, postcss, walk, parseJS, selectedFile, knownSlot, origin, add, storybook, entries = [], scopeIncomplete = true, scopeRecords = records, implementationFiles = [] }) { const checks = []; const scope = classScope(scopeRecords, entries, walk, origin, scopeIncomplete, implementationFiles, parseJS); const record = (file, node, selector, component, property, value, important = false, applicability = { status: 'POSSIBLE' }) => { const color = rawColor.test(value); const spacing = geometry.test(property) && /(?:^|[\s(+])[-+]?(?:\d*\.)?\d+(?:px|rem|em)\b/.test(value) && !/^(?:0(?:px|rem|em)?\s*)+$/.test(value); const override = Boolean(component); const global = /(?:^|[,\s])(?:html|body|:root|\*)(?:$|[,\s.#:[>])/.test(selector); const forbidden = override && applicability.status !== 'NOT APPLICABLE' && (rawColor.test(withoutVariables(value)) || spacing); const intent = override || color || spacing || important; const ruleId = forbidden ? 'STYLE-001' : 'STYLE-002'; if (forbidden && !storybook) add('FAIL', ruleId, file, node, 'Hardcoded color / spacing / radius overrides an HM Component; use its documented Token / Prop contract'); const relative = path.relative(target, file).replaceAll('\\', '/'); checks.push({ id: `${relative}:${lineOf(node)}:${selector}:${property}:${value}`, file: relative, line: lineOf(node), ruleId, component: component ?? null, selector, property, value, important, tokens: [...value.matchAll(/var\(\s*(--[\w-]+)/g)].map(m => m[1]), scope: global ? 'GLOBAL' : 'COMPONENT_SCOPED', applicability, staticStatus: forbidden && !storybook ? 'FAIL' : 'PASS', runtimeStatus: 'NOT VERIFIED', designIntentStatus: intent ? 'NOT VERIFIED' : 'PASS', reason: intent ? 'Literal styling / Component override requires a documented design decision; computed equality is not approval' : 'No ambiguous override detected; cascade / actual application requires browser evidence' }); }; for (const r of records.filter(r => selectedFile(r.file))) { if (r.css) { try { const ast = postcss.parse(fs.readFileSync(r.file, 'utf8')); ast.walkDecls(n => { const selector = n.parent?.type === 'rule' ? n.parent.selector : `@${n.parent?.name ?? 'declaration'}`; const slots = [...selector.matchAll(/data-slot\s*=\s*["']?([\w-]+)/g)].map(m => m[1]); const classes = [...selector.matchAll(/\.([A-Za-z_][\w-]*)/g)].map(m => m[1]); const component = slots.find(knownSlot) ?? classes.filter(name => name.startsWith('hm-') && knownSlot(name.slice(3))).map(name => scope.component(name)).find(Boolean); record(r.file, n, selector, component, n.prop, n.value, n.important, scope.evidence(selector)); }); } catch { /* Existing CSS-005 retains parse uncertainty. */ } } const constants = r.ast ? localConstants(r.ast, walk) : new Map(); if (r.ast) walk(r.ast, (n, parent) => { if (n.type !== 'JSXAttribute' || !['style', 'className'].includes(n.name.name)) return; const tag = parent.name?.type === 'JSXMemberExpression' ? `${parent.name.object.name}.${parent.name.property.name}` : parent.name?.name; const identity = origin(r.file, tag); const component = identity?.pkg === '@hm-design/ui' ? identity.name : null; if (n.name.name === 'className') { const values = knownStyleStrings(n.value?.expression ?? n.value, constants); for (const v of values ?? []) for (const m of v.matchAll(/(?:^|[\s:])([\w-]+)-\[([^\]]+)\]/g)) { const property = m[1] === 'gap' ? 'gap' : /^(?:p|m)[xytrbl]?$/.test(m[1]) ? `${m[1][0] === 'p' ? 'padding' : 'margin'}${({ x: '-inline', y: '-block', t: '-top', r: '-right', b: '-bottom', l: '-left' })[m[1][1]] ?? ''}` : m[1] === 'rounded' ? 'border-radius' : 'utility-' + m[1]; record(r.file, n, `<${tag ?? 'element'}>`, component, property, m[2]); } return; } const value = n.value?.expression; if (value?.type !== 'ObjectExpression') return; for (const p of value.properties) { if (p.type !== 'ObjectProperty' || p.computed) continue; const key = p.key.name ?? p.key.value; const values = knownStyleStrings(p.value, constants); const property = String(key).replace(/[A-Z]/g, c => `-${c.toLowerCase()}`); for (const v of values ?? []) record(r.file, p, `<${tag ?? 'element'}>`, component, property, geometry.test(property) && /^\d+(?:\.\d+)?$/.test(v) ? `${v}px` : v); } }); } return [...new Map(checks.map(c => [c.id, c])).values()]; }