feat: integrate latest HMDesign design system updates
This commit is contained in:
1 parent
99faec189e
commit
1c4e0bb9a1
209 files changed
+8845
-552
No files matched your search
@@ -0,0 +1,230 @@
|
||||
// Conservative render-value analysis. Never executes consumer code.
|
||||
// Only immutable local values, direct component calls and literal conditions
|
||||
// prove a branch absent. Unsupported flows retain NOT VERIFIED navigation.
|
||||
export function navigationAnalysis(records, roots, origin, walk) {
|
||||
const unknown = Symbol('unknown');
|
||||
const state = new Map();
|
||||
const sidebarValues = new Map();
|
||||
const identities = new Map();
|
||||
const modules = new Map(records.filter(r => r.ast).map(r => [r.file, r]));
|
||||
const active = new Set();
|
||||
const rank = { dead: 0, maybe: 1, active: 2 };
|
||||
const mark = (node, value) => { if (!state.has(node) || rank[value] > rank[state.get(node)]) state.set(node, value); };
|
||||
const fallback = (node, certainty = 'maybe') => walk(node, n => { if (n.type === 'JSXOpeningElement' || n.type === 'CallExpression' && /createElement|^(?:_?jsx|_?jsxs|jsxDEV)$/.test(n.callee?.name ?? n.callee?.property?.name ?? '')) mark(n, certainty); });
|
||||
const unwrap = n => ['TSAsExpression', 'TSSatisfiesExpression', 'TSNonNullExpression', 'ParenthesizedExpression'].includes(n?.type) ? unwrap(n.expression) : n;
|
||||
function uncertain(node, env, seen = new Set()) {
|
||||
if (!node || seen.has(node)) return;
|
||||
seen.add(node); fallback(node);
|
||||
walk(node, x => {
|
||||
if (x.type !== 'Identifier') return;
|
||||
const b = env.get(x.name);
|
||||
if (b?.fn) uncertain(b.fn, b.env, seen);
|
||||
if (b?.expr) uncertain(b.expr, b.env, seen);
|
||||
});
|
||||
}
|
||||
function invalidate(body, env) {
|
||||
const taint = (name, seen = new Set()) => {
|
||||
if (seen.has(name)) return;
|
||||
seen.add(name);
|
||||
const previous = env.get(name);
|
||||
if (previous?.expr) {
|
||||
uncertain(previous.expr, previous.env);
|
||||
if (previous.expr.type === 'Identifier') taint(previous.expr.name, seen);
|
||||
}
|
||||
env.set(name, unknown);
|
||||
};
|
||||
const objectBinding = (name, seen = new Set()) => {
|
||||
if (seen.has(name)) return false;
|
||||
seen.add(name); const b = env.get(name);
|
||||
return b?.expr?.type === 'ObjectExpression' || b?.expr?.type === 'Identifier' && objectBinding(b.expr.name, seen) || b && typeof b === 'object' && !b.expr && !b.fn;
|
||||
};
|
||||
walk({ type: 'BlockStatement', body }, n => {
|
||||
if (n.type === 'AssignmentExpression' || n.type === 'UpdateExpression') {
|
||||
let lhs = n.left ?? n.argument;
|
||||
while (lhs?.type === 'MemberExpression') lhs = lhs.object;
|
||||
if (lhs?.type === 'Identifier') taint(lhs.name);
|
||||
}
|
||||
// An opaque call can mutate object/props arguments. This may over-report
|
||||
// uncertainty, but must never prove a dead branch from a stale value.
|
||||
if (n.type === 'CallExpression') for (const arg of n.arguments) if (arg.type === 'Identifier' && env.has(arg.name)) {
|
||||
if (objectBinding(arg.name)) taint(arg.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
function environment(r, body, parent = new Map()) {
|
||||
const env = new Map(parent);
|
||||
for (let stmt of body) {
|
||||
stmt = stmt.declaration ?? stmt;
|
||||
if (stmt.type === 'FunctionDeclaration' && stmt.id) env.set(stmt.id.name, { fn: stmt, r, env });
|
||||
if (stmt.type === 'VariableDeclaration') for (const d of stmt.declarations) {
|
||||
if (d.id.type === 'Identifier') env.set(d.id.name, { expr: d.init, r, env, mutable: stmt.kind !== 'const' });
|
||||
else { fallback(d.init); for (const id of d.id.elements ?? []) if (id?.type === 'Identifier') env.set(id.name, unknown); }
|
||||
}
|
||||
}
|
||||
// Even const objects / parameters can be mutated. Do not infer through writes.
|
||||
invalidate(body, env);
|
||||
return env;
|
||||
}
|
||||
for (const r of modules.values()) r.navEnv = environment(r, r.ast.program.body);
|
||||
function exported(r, name, seen = new Set()) {
|
||||
if (!r || seen.has(`${r.file}:${name}`)) return unknown;
|
||||
seen.add(`${r.file}:${name}`);
|
||||
const b = r.exports.get(name);
|
||||
if (b?.edge) return exported(modules.get(b.edge.resolved), b.imported, seen);
|
||||
if (b) {
|
||||
if (b.local === 'default') {
|
||||
const d = r.ast.program.body.find(n => n.type === 'ExportDefaultDeclaration')?.declaration;
|
||||
return value(d, r, r.navEnv);
|
||||
}
|
||||
return value({ type: 'Identifier', name: b.local }, r, r.navEnv);
|
||||
}
|
||||
for (const e of r.stars) { const found = exported(modules.get(e.resolved), name, seen); if (found !== unknown) return found; }
|
||||
return unknown;
|
||||
}
|
||||
const evaluating = new Set();
|
||||
function value(raw, r, env) {
|
||||
const n = unwrap(raw);
|
||||
if (!n) return undefined;
|
||||
if (['StringLiteral', 'NumericLiteral', 'BooleanLiteral'].includes(n.type)) return n.value;
|
||||
if (n.type === 'NullLiteral') return null;
|
||||
if (n.type === 'TemplateLiteral' && !n.expressions.length) return n.quasis[0].value.cooked;
|
||||
if (n.type === 'Identifier') {
|
||||
if (env.has(n.name)) {
|
||||
const v = env.get(n.name);
|
||||
if (!v?.expr) return v;
|
||||
if (v.mutable || evaluating.has(v)) { uncertain(v.expr, v.env); return unknown; }
|
||||
evaluating.add(v); const result = value(v.expr, v.r, v.env); evaluating.delete(v); return result;
|
||||
}
|
||||
if (n.name === 'undefined') return undefined;
|
||||
const official = origin(r.file, n.name);
|
||||
if (official?.pkg) return { official };
|
||||
const b = r.bindings.get(n.name);
|
||||
if (b?.edge) return exported(modules.get(b.edge.resolved), b.imported);
|
||||
return unknown;
|
||||
}
|
||||
if (['ArrowFunctionExpression', 'FunctionExpression', 'FunctionDeclaration'].includes(n.type)) return { fn: n, r, env };
|
||||
if (n.type === 'JSXElement' || n.type === 'JSXFragment') return { jsx: n, r, env };
|
||||
if (n.type === 'ObjectExpression') {
|
||||
const obj = Object.create(null);
|
||||
for (const p of n.properties) { if (p.type !== 'ObjectProperty' || p.computed) return unknown; obj[p.key.name ?? p.key.value] = value(p.value, r, env); }
|
||||
return obj;
|
||||
}
|
||||
if (n.type === 'MemberExpression') {
|
||||
if (!n.computed && n.object.type === 'Identifier') {
|
||||
const official = origin(r.file, `${n.object.name}.${n.property.name}`);
|
||||
if (official?.pkg && !env.has(n.object.name)) return { official };
|
||||
}
|
||||
const obj = value(n.object, r, env); const key = n.computed ? value(n.property, r, env) : n.property.name;
|
||||
if (obj?.fn || obj?.official || obj?.jsx || obj?.branches) return unknown;
|
||||
return obj !== unknown && obj != null && key !== unknown ? obj[key] : unknown;
|
||||
}
|
||||
if (n.type === 'UnaryExpression' && n.operator === '!') { const v = value(n.argument, r, env); return v === unknown || v?.branches ? unknown : !v; }
|
||||
if (n.type === 'BinaryExpression' && ['===', '!==', '==', '!='].includes(n.operator)) {
|
||||
const a = value(n.left, r, env), b = value(n.right, r, env);
|
||||
if (a === unknown || b === unknown || a !== null && ['object', 'function'].includes(typeof a) || b !== null && ['object', 'function'].includes(typeof b)) return unknown;
|
||||
return n.operator === '===' ? a === b : n.operator === '!==' ? a !== b : n.operator === '==' ? a == b : a != b;
|
||||
}
|
||||
if (n.type === 'ConditionalExpression') { const test = value(n.test, r, env); return test === unknown || test?.branches ? { branches: [n.consequent, n.alternate], r, env } : value(test ? n.consequent : n.alternate, r, env); }
|
||||
if (n.type === 'LogicalExpression') {
|
||||
const a = value(n.left, r, env);
|
||||
if (a === unknown || a?.branches) return { branches: [n.left, n.right], r, env };
|
||||
return n.operator === '&&' ? a && value(n.right, r, env) : n.operator === '||' ? a || value(n.right, r, env) : a ?? value(n.right, r, env);
|
||||
}
|
||||
if (n.type === 'ArrayExpression') return { branches: n.elements, r, env, definite: true };
|
||||
uncertain(n, env);
|
||||
return unknown;
|
||||
}
|
||||
function bind(pattern, v, r, env) {
|
||||
if (!pattern) return;
|
||||
if (pattern.type === 'Identifier') env.set(pattern.name, v);
|
||||
else if (pattern.type === 'AssignmentPattern') bind(pattern.left, v === undefined ? value(pattern.right, r, env) : v, r, env);
|
||||
else if (pattern.type === 'ObjectPattern') for (const p of pattern.properties) {
|
||||
if (p.type === 'ObjectProperty' && !p.computed) bind(p.value, v === unknown || v == null ? unknown : v[p.key.name ?? p.key.value], r, env);
|
||||
else if (p.argument) bind(p.argument, unknown, r, env);
|
||||
}
|
||||
}
|
||||
function invoke(v, props, certainty) {
|
||||
if (!v?.fn) return;
|
||||
const { fn, r } = v;
|
||||
if (active.has(fn)) { fallback(fn); return; }
|
||||
active.add(fn);
|
||||
// Unused JSX initializers are constructed values, not mounted elements.
|
||||
fallback(fn, 'dead');
|
||||
const env = environment(r, fn.body.body ?? [], v.env);
|
||||
fn.params.forEach((p, i) => bind(p, i === 0 ? props : unknown, r, env));
|
||||
invalidate(fn.body.body ?? [], env);
|
||||
if (fn.body.type !== 'BlockStatement') render(fn.body, r, env, certainty);
|
||||
else statements(fn.body.body, r, env, certainty);
|
||||
active.delete(fn);
|
||||
}
|
||||
function statements(body, r, env, certainty) {
|
||||
for (const n of body) {
|
||||
if (n.type === 'ReturnStatement') { render(n.argument, r, env, certainty); return; }
|
||||
if (n.type === 'IfStatement') {
|
||||
const t = value(n.test, r, env);
|
||||
const unproven = t === unknown || t?.branches;
|
||||
const branches = unproven ? [n.consequent, n.alternate] : [t ? n.consequent : n.alternate];
|
||||
for (const branch of branches.filter(Boolean)) statements(branch.type === 'BlockStatement' ? branch.body : [branch], r, environment(r, branch.type === 'BlockStatement' ? branch.body : [branch], env), unproven ? 'maybe' : certainty);
|
||||
// Early-return control flow requires a full CFG; retain uncertainty after it.
|
||||
if (branches.some(b => { let found = false; walk(b, x => { if (x.type === 'ReturnStatement') found = true; }); return found; })) certainty = 'maybe';
|
||||
} else if (!['VariableDeclaration', 'FunctionDeclaration', 'ExpressionStatement', 'EmptyStatement', 'TSTypeAliasDeclaration', 'TSInterfaceDeclaration'].includes(n.type)) fallback(n);
|
||||
// Effects / callbacks can mount values outside the direct JSX return graph.
|
||||
if (n.type === 'ExpressionStatement') uncertain(n, env);
|
||||
}
|
||||
}
|
||||
function consume(v, certainty) {
|
||||
if (v?.jsx) render(v.jsx, v.r, v.env, certainty);
|
||||
else if (v?.branches) for (const b of v.branches) render(b, v.r, v.env, v.definite ? certainty : 'maybe');
|
||||
else if (v?.fn) fallback(v.fn);
|
||||
else if (v && typeof v === 'object') Object.values(v).forEach(item => consume(item, 'maybe'));
|
||||
}
|
||||
function render(raw, r, env, certainty = 'active') {
|
||||
const n = unwrap(raw);
|
||||
if (!n) return;
|
||||
if (n.type === 'JSXExpressionContainer') { render(n.expression, r, env, certainty); return; }
|
||||
if (n.type === 'JSXFragment') { n.children.forEach(c => render(c, r, env, certainty)); return; }
|
||||
if (n.type === 'JSXElement') {
|
||||
const o = n.openingElement; mark(o, certainty);
|
||||
const jsxName = name => name.type === 'JSXIdentifier' ? name.name : `${jsxName(name.object)}.${name.property.name}`;
|
||||
const name = jsxName(o.name);
|
||||
const component = name.includes('.') ? value({type:'MemberExpression',object:{type:'Identifier',name:o.name.object.name},property:{type:'Identifier',name:o.name.property.name},computed:false}, r, env) : value({ type: 'Identifier', name }, r, env);
|
||||
const id = component?.official ?? origin(r.file, name);
|
||||
if (component?.official) {
|
||||
if (!identities.has(o)) identities.set(o, []);
|
||||
if (!identities.get(o).some(x => x.name === id.name && x.pkg === id.pkg)) identities.get(o).push(id);
|
||||
}
|
||||
const props = {};
|
||||
let spread = false;
|
||||
for (const a of o.attributes) {
|
||||
if (a.type === 'JSXSpreadAttribute') { spread = true; uncertain(a.argument, env); continue; }
|
||||
props[a.name.name] = a.value == null ? true : value(a.value.type === 'JSXExpressionContainer' ? a.value.expression : a.value, r, env);
|
||||
}
|
||||
if (name === 'DashboardLayout' || id?.name === 'DashboardLayout') {
|
||||
const candidate = spread ? 'unknown' : !('sidebar' in props) || [undefined, null, false].includes(props.sidebar) ? 'empty' : props.sidebar === unknown || props.sidebar?.branches ? 'unknown' : 'present';
|
||||
const sv = certainty === 'maybe' && candidate === 'present' ? 'unknown' : candidate;
|
||||
const old = sidebarValues.get(o);
|
||||
sidebarValues.set(o, old === 'present' || sv === 'present' ? 'present' : old === 'unknown' || sv === 'unknown' ? 'unknown' : 'empty');
|
||||
}
|
||||
if (component?.fn) {
|
||||
if (n.children.some(c => c.type !== 'JSXText')) props.children = { jsx: { type: 'JSXFragment', children: n.children }, r, env };
|
||||
invoke(component, spread ? unknown : props, certainty);
|
||||
} else {
|
||||
// Official and external components may forward render-valued props/children.
|
||||
Object.values(props).forEach(v => consume(v, certainty));
|
||||
n.children.forEach(c => render(c, r, env, certainty));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const v = value(n, r, env);
|
||||
if (v === unknown) uncertain(n, env);
|
||||
else consume(v, certainty);
|
||||
}
|
||||
let mounted = false;
|
||||
for (const r of modules.values()) walk(r.ast, n => {
|
||||
if (n.type === 'CallExpression' && (n.callee.type === 'MemberExpression' && n.callee.property.name === 'render' || n.callee.name === 'hydrateRoot')) {
|
||||
mounted = true; render(n.callee.name === 'hydrateRoot' ? n.arguments[1] : n.arguments[0], r, r.navEnv);
|
||||
}
|
||||
});
|
||||
if (!mounted) for (const file of roots) invoke(exported(modules.get(file), 'default'), {}, 'active');
|
||||
return { state, sidebarValues, identities };
|
||||
}
|
||||
Reference in new issue
Block a user