425 lines
10 KiB
TypeScript

// should only use types from @babel/types
// do not import runtime methods
import type {
Identifier,
Node,
Function,
ObjectProperty,
BlockStatement,
Program
} from '@babel/types'
import { walk } from 'estree-walker'
export function walkIdentifiers(
root: Node,
onIdentifier: (
node: Identifier,
parent: Node,
parentStack: Node[],
isReference: boolean,
isLocal: boolean
) => void,
includeAll = false,
parentStack: Node[] = [],
knownIds: Record<string, number> = Object.create(null)
) {
if (__BROWSER__) {
return
}
const rootExp =
root.type === 'Program' &&
root.body[0].type === 'ExpressionStatement' &&
root.body[0].expression
;(walk as any)(root, {
enter(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
parent && parentStack.push(parent)
if (
parent &&
parent.type.startsWith('TS') &&
parent.type !== 'TSAsExpression' &&
parent.type !== 'TSNonNullExpression' &&
parent.type !== 'TSTypeAssertion'
) {
return this.skip()
}
if (node.type === 'Identifier') {
const isLocal = !!knownIds[node.name]
const isRefed = isReferencedIdentifier(node, parent!, parentStack)
if (includeAll || (isRefed && !isLocal)) {
onIdentifier(node, parent!, parentStack, isRefed, isLocal)
}
} else if (
node.type === 'ObjectProperty' &&
parent!.type === 'ObjectPattern'
) {
// mark property in destructure pattern
;(node as any).inPattern = true
} else if (isFunctionType(node)) {
// walk function expressions and add its arguments to known identifiers
// so that we don't prefix them
walkFunctionParams(node, id => markScopeIdentifier(node, id, knownIds))
} else if (node.type === 'BlockStatement') {
// #3445 record block-level local variables
walkBlockDeclarations(node, id =>
markScopeIdentifier(node, id, knownIds)
)
}
},
leave(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
parent && parentStack.pop()
if (node !== rootExp && node.scopeIds) {
for (const id of node.scopeIds) {
knownIds[id]--
if (knownIds[id] === 0) {
delete knownIds[id]
}
}
}
}
})
}
export function isReferencedIdentifier(
id: Identifier,
parent: Node | null,
parentStack: Node[]
) {
if (__BROWSER__) {
return false
}
if (!parent) {
return true
}
// is a special keyword but parsed as identifier
if (id.name === 'arguments') {
return false
}
if (isReferenced(id, parent)) {
return true
}
// babel's isReferenced check returns false for ids being assigned to, so we
// need to cover those cases here
switch (parent.type) {
case 'AssignmentExpression':
case 'AssignmentPattern':
return true
case 'ObjectPattern':
case 'ArrayPattern':
return isInDestructureAssignment(parent, parentStack)
}
return false
}
export function isInDestructureAssignment(
parent: Node,
parentStack: Node[]
): boolean {
if (
parent &&
(parent.type === 'ObjectProperty' || parent.type === 'ArrayPattern')
) {
let i = parentStack.length
while (i--) {
const p = parentStack[i]
if (p.type === 'AssignmentExpression') {
return true
} else if (p.type !== 'ObjectProperty' && !p.type.endsWith('Pattern')) {
break
}
}
}
return false
}
export function walkFunctionParams(
node: Function,
onIdent: (id: Identifier) => void
) {
for (const p of node.params) {
for (const id of extractIdentifiers(p)) {
onIdent(id)
}
}
}
export function walkBlockDeclarations(
block: BlockStatement | Program,
onIdent: (node: Identifier) => void
) {
for (const stmt of block.body) {
if (stmt.type === 'VariableDeclaration') {
if (stmt.declare) continue
for (const decl of stmt.declarations) {
for (const id of extractIdentifiers(decl.id)) {
onIdent(id)
}
}
} else if (
stmt.type === 'FunctionDeclaration' ||
stmt.type === 'ClassDeclaration'
) {
if (stmt.declare || !stmt.id) continue
onIdent(stmt.id)
}
}
}
export function extractIdentifiers(
param: Node,
nodes: Identifier[] = []
): Identifier[] {
switch (param.type) {
case 'Identifier':
nodes.push(param)
break
case 'MemberExpression':
let object: any = param
while (object.type === 'MemberExpression') {
object = object.object
}
nodes.push(object)
break
case 'ObjectPattern':
for (const prop of param.properties) {
if (prop.type === 'RestElement') {
extractIdentifiers(prop.argument, nodes)
} else {
extractIdentifiers(prop.value, nodes)
}
}
break
case 'ArrayPattern':
param.elements.forEach(element => {
if (element) extractIdentifiers(element, nodes)
})
break
case 'RestElement':
extractIdentifiers(param.argument, nodes)
break
case 'AssignmentPattern':
extractIdentifiers(param.left, nodes)
break
}
return nodes
}
function markScopeIdentifier(
node: Node & { scopeIds?: Set<string> },
child: Identifier,
knownIds: Record<string, number>
) {
const { name } = child
if (node.scopeIds && node.scopeIds.has(name)) {
return
}
if (name in knownIds) {
knownIds[name]++
} else {
knownIds[name] = 1
}
;(node.scopeIds || (node.scopeIds = new Set())).add(name)
}
export const isFunctionType = (node: Node): node is Function => {
return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
}
export const isStaticProperty = (node: Node): node is ObjectProperty =>
node &&
(node.type === 'ObjectProperty' || node.type === 'ObjectMethod') &&
!node.computed
export const isStaticPropertyKey = (node: Node, parent: Node) =>
isStaticProperty(parent) && parent.key === node
/**
* Copied from https://github.com/babel/babel/blob/main/packages/babel-types/src/validators/isReferenced.ts
* To avoid runtime dependency on @babel/types (which includes process references)
* This file should not change very often in babel but we may need to keep it
* up-to-date from time to time.
*
* https://github.com/babel/babel/blob/main/LICENSE
*
*/
function isReferenced(node: Node, parent: Node, grandparent?: Node): boolean {
switch (parent.type) {
// yes: PARENT[NODE]
// yes: NODE.child
// no: parent.NODE
case 'MemberExpression':
case 'OptionalMemberExpression':
if (parent.property === node) {
return !!parent.computed
}
return parent.object === node
case 'JSXMemberExpression':
return parent.object === node
// no: let NODE = init;
// yes: let id = NODE;
case 'VariableDeclarator':
return parent.init === node
// yes: () => NODE
// no: (NODE) => {}
case 'ArrowFunctionExpression':
return parent.body === node
// no: class { #NODE; }
// no: class { get #NODE() {} }
// no: class { #NODE() {} }
// no: class { fn() { return this.#NODE; } }
case 'PrivateName':
return false
// no: class { NODE() {} }
// yes: class { [NODE]() {} }
// no: class { foo(NODE) {} }
case 'ClassMethod':
case 'ClassPrivateMethod':
case 'ObjectMethod':
if (parent.key === node) {
return !!parent.computed
}
return false
// yes: { [NODE]: "" }
// no: { NODE: "" }
// depends: { NODE }
// depends: { key: NODE }
case 'ObjectProperty':
if (parent.key === node) {
return !!parent.computed
}
// parent.value === node
return !grandparent || grandparent.type !== 'ObjectPattern'
// no: class { NODE = value; }
// yes: class { [NODE] = value; }
// yes: class { key = NODE; }
case 'ClassProperty':
if (parent.key === node) {
return !!parent.computed
}
return true
case 'ClassPrivateProperty':
return parent.key !== node
// no: class NODE {}
// yes: class Foo extends NODE {}
case 'ClassDeclaration':
case 'ClassExpression':
return parent.superClass === node
// yes: left = NODE;
// no: NODE = right;
case 'AssignmentExpression':
return parent.right === node
// no: [NODE = foo] = [];
// yes: [foo = NODE] = [];
case 'AssignmentPattern':
return parent.right === node
// no: NODE: for (;;) {}
case 'LabeledStatement':
return false
// no: try {} catch (NODE) {}
case 'CatchClause':
return false
// no: function foo(...NODE) {}
case 'RestElement':
return false
case 'BreakStatement':
case 'ContinueStatement':
return false
// no: function NODE() {}
// no: function foo(NODE) {}
case 'FunctionDeclaration':
case 'FunctionExpression':
return false
// no: export NODE from "foo";
// no: export * as NODE from "foo";
case 'ExportNamespaceSpecifier':
case 'ExportDefaultSpecifier':
return false
// no: export { foo as NODE };
// yes: export { NODE as foo };
// no: export { NODE as foo } from "foo";
case 'ExportSpecifier':
// @ts-expect-error
if (grandparent?.source) {
return false
}
return parent.local === node
// no: import NODE from "foo";
// no: import * as NODE from "foo";
// no: import { NODE as foo } from "foo";
// no: import { foo as NODE } from "foo";
// no: import NODE from "bar";
case 'ImportDefaultSpecifier':
case 'ImportNamespaceSpecifier':
case 'ImportSpecifier':
return false
// no: import "foo" assert { NODE: "json" }
case 'ImportAttribute':
return false
// no: <div NODE="foo" />
case 'JSXAttribute':
return false
// no: [NODE] = [];
// no: ({ NODE }) = [];
case 'ObjectPattern':
case 'ArrayPattern':
return false
// no: new.NODE
// no: NODE.target
case 'MetaProperty':
return false
// yes: type X = { somePropert: NODE }
// no: type X = { NODE: OtherType }
case 'ObjectTypeProperty':
return parent.key !== node
// yes: enum X { Foo = NODE }
// no: enum X { NODE }
case 'TSEnumMember':
return parent.id !== node
// yes: { [NODE]: value }
// no: { NODE: value }
case 'TSPropertySignature':
if (parent.key === node) {
return !!parent.computed
}
return true
}
return true
}