|
| 1 | +import type { NodePath } from '@babel/traverse' |
| 2 | +import * as t from '@babel/types' |
| 3 | +import { propertyName } from '#design-diff/ast' |
| 4 | +import { immutableCollection } from '#design-diff/mutations' |
| 5 | +import type { Resolver } from '#design-diff/resolve' |
| 6 | + |
| 7 | +interface Source { |
| 8 | + path: NodePath |
| 9 | + file: string |
| 10 | +} |
| 11 | +const child = (path: NodePath, name: string) => path.get(name) as NodePath |
| 12 | +const children = (path: NodePath, name: string) => path.get(name) as NodePath[] |
| 13 | + |
| 14 | +/** Bound computed keys from immutable literals and static Object.entries/values loops. No loop or source function executes. */ |
| 15 | +export function finiteKeys(path: NodePath, file: string, resolver: Resolver) { |
| 16 | + const dependencies = new Set<string>() |
| 17 | + let steps = 0 |
| 18 | + let mutable = false |
| 19 | + const sources = (source: Source, active = new Set<t.Node>()): Source[] | undefined => { |
| 20 | + const { path, file } = source |
| 21 | + if (!path.node || ++steps > 512 || active.size > 24 || active.has(path.node)) return undefined |
| 22 | + const seen = new Set([...active, path.node]) |
| 23 | + dependencies.add(file) |
| 24 | + const next = (path: NodePath, from = file) => sources({ path, file: from }, seen) |
| 25 | + if (path.isTSAsExpression() || path.isTSSatisfiesExpression() || path.isTSNonNullExpression()) |
| 26 | + return next(child(path, 'expression')) |
| 27 | + if (path.isIdentifier()) { |
| 28 | + const binding = path.scope.getBinding(path.node.name) |
| 29 | + if (!binding) return undefined |
| 30 | + if (!immutableCollection(binding)) { |
| 31 | + mutable = true |
| 32 | + return undefined |
| 33 | + } |
| 34 | + const bound = binding.path |
| 35 | + if (bound.isVariableDeclarator()) { |
| 36 | + if (bound.node.init) return next(child(bound, 'init')) |
| 37 | + const loop = bound.parentPath.parentPath |
| 38 | + if (!loop?.isForOfStatement() || loop.node.await) return undefined |
| 39 | + let right = child(loop, 'right') |
| 40 | + while ( |
| 41 | + right.isTSAsExpression() || |
| 42 | + right.isTSSatisfiesExpression() || |
| 43 | + right.isTSNonNullExpression() |
| 44 | + ) |
| 45 | + right = child(right, 'expression') |
| 46 | + if (!right.isCallExpression()) return undefined |
| 47 | + const callee = child(right, 'callee') |
| 48 | + if ( |
| 49 | + !callee.isMemberExpression() || |
| 50 | + callee.node.computed || |
| 51 | + !t.isIdentifier(callee.node.object, { name: 'Object' }) || |
| 52 | + callee.scope.getBinding('Object') |
| 53 | + ) |
| 54 | + return undefined |
| 55 | + const method = propertyName(callee.node.property) |
| 56 | + const args = children(right, 'arguments') |
| 57 | + if (args.length !== 1 || !['entries', 'values'].includes(method)) return undefined |
| 58 | + let index = -1 |
| 59 | + if (method === 'entries' && t.isArrayPattern(bound.node.id)) |
| 60 | + index = bound.node.id.elements.findIndex((node) => |
| 61 | + t.isIdentifier(node, { name: path.node.name }) |
| 62 | + ) |
| 63 | + if (method === 'values' && t.isIdentifier(bound.node.id, { name: path.node.name })) |
| 64 | + index = 1 |
| 65 | + if (index !== 1) return undefined |
| 66 | + const records = next(args[0]) |
| 67 | + if (!records || records.length > 128) return undefined |
| 68 | + const result: Source[] = [] |
| 69 | + for (const record of records) { |
| 70 | + if (!record.path.isObjectExpression()) return undefined |
| 71 | + for (const property of children(record.path, 'properties')) { |
| 72 | + if (!property.isObjectProperty() || property.node.computed) return undefined |
| 73 | + result.push({ path: child(property, 'value'), file: record.file }) |
| 74 | + if (result.length > 128) return undefined |
| 75 | + } |
| 76 | + } |
| 77 | + return result |
| 78 | + } |
| 79 | + if (bound.isImportSpecifier() || bound.isImportDefaultSpecifier()) { |
| 80 | + const declaration = bound.parentPath |
| 81 | + if (!declaration.isImportDeclaration()) return undefined |
| 82 | + const target = resolver.tree.resolve(file, declaration.node.source.value) |
| 83 | + const name = bound.isImportSpecifier() ? propertyName(bound.node.imported) : 'default' |
| 84 | + const resolved = target ? resolver.tree.graph?.resolvedExport(target, name) : undefined |
| 85 | + if (!resolved?.origin) return undefined |
| 86 | + for (const route of resolved.routes) dependencies.add(route) |
| 87 | + const exported = resolver.module(resolved.origin.file).exports.get(resolved.origin.exported) |
| 88 | + if ( |
| 89 | + exported?.parentPath?.isVariableDeclarator() && |
| 90 | + t.isIdentifier(exported.parentPath.node.id) |
| 91 | + ) { |
| 92 | + const binding = exported.scope.getBinding(exported.parentPath.node.id.name) |
| 93 | + if (binding && !immutableCollection(binding)) { |
| 94 | + mutable = true |
| 95 | + return undefined |
| 96 | + } |
| 97 | + } |
| 98 | + return exported ? next(exported, resolved.origin.file) : undefined |
| 99 | + } |
| 100 | + return undefined |
| 101 | + } |
| 102 | + if ( |
| 103 | + path.isMemberExpression() && |
| 104 | + (!path.node.computed || t.isStringLiteral(path.node.property)) |
| 105 | + ) { |
| 106 | + const key = propertyName(path.node.property) |
| 107 | + const records = next(child(path, 'object')) |
| 108 | + if (!records || records.length > 128) return undefined |
| 109 | + const result: Source[] = [] |
| 110 | + for (const record of records) { |
| 111 | + if (!record.path.isObjectExpression()) return undefined |
| 112 | + const properties = children(record.path, 'properties') |
| 113 | + if (properties.some((prop) => !prop.isObjectProperty() || prop.node.computed)) |
| 114 | + return undefined |
| 115 | + const property = properties |
| 116 | + .reverse() |
| 117 | + .find((prop) => prop.isObjectProperty() && propertyName(prop.node.key) === key) |
| 118 | + if (!property) return undefined |
| 119 | + const values = sources({ path: child(property, 'value'), file: record.file }, seen) |
| 120 | + if (!values) return undefined |
| 121 | + result.push(...values) |
| 122 | + } |
| 123 | + return result |
| 124 | + } |
| 125 | + return [source] |
| 126 | + } |
| 127 | + try { |
| 128 | + const values = sources({ path, file }) |
| 129 | + if (!values?.length || values.length > 128) |
| 130 | + return mutable |
| 131 | + ? { keys: undefined, dependencies: [...dependencies].sort(), mutable: true } |
| 132 | + : undefined |
| 133 | + const keys: (string | number)[] = [] |
| 134 | + for (const value of values) { |
| 135 | + if (!value.path.isStringLiteral() && !value.path.isNumericLiteral()) return undefined |
| 136 | + keys.push(value.path.node.value) |
| 137 | + } |
| 138 | + return { keys: [...new Set(keys)], dependencies: [...dependencies].sort() } |
| 139 | + } catch { |
| 140 | + return undefined |
| 141 | + } |
| 142 | +} |
0 commit comments