mirror of https://github.com/vuejs/core.git
513 lines
13 KiB
TypeScript
513 lines
13 KiB
TypeScript
// should only use types from @babel/types
|
|
// do not import runtime methods
|
|
import type {
|
|
BlockStatement,
|
|
ForInStatement,
|
|
ForOfStatement,
|
|
ForStatement,
|
|
Function,
|
|
Identifier,
|
|
Node,
|
|
ObjectProperty,
|
|
Program,
|
|
} from '@babel/types'
|
|
import { walk } from 'estree-walker'
|
|
|
|
/**
|
|
* Return value indicates whether the AST walked can be a constant
|
|
*/
|
|
export function walkIdentifiers(
|
|
root: Node,
|
|
onIdentifier: (
|
|
node: Identifier,
|
|
parent: Node | null,
|
|
parentStack: Node[],
|
|
isReference: boolean,
|
|
isLocal: boolean,
|
|
) => void,
|
|
includeAll = false,
|
|
parentStack: Node[] = [],
|
|
knownIds: Record<string, number> = Object.create(null),
|
|
): void {
|
|
if (__BROWSER__) {
|
|
return
|
|
}
|
|
|
|
const rootExp =
|
|
root.type === 'Program'
|
|
? root.body[0].type === 'ExpressionStatement' && root.body[0].expression
|
|
: root
|
|
|
|
walk(root, {
|
|
enter(node: Node & { scopeIds?: Set<string> }, parent: Node | null) {
|
|
parent && parentStack.push(parent)
|
|
if (
|
|
parent &&
|
|
parent.type.startsWith('TS') &&
|
|
!TS_NODE_TYPES.includes(parent.type)
|
|
) {
|
|
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' &&
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
parent?.type === 'ObjectPattern'
|
|
) {
|
|
// mark property in destructure pattern
|
|
;(node as any).inPattern = true
|
|
} else if (isFunctionType(node)) {
|
|
if (node.scopeIds) {
|
|
node.scopeIds.forEach(id => markKnownIds(id, knownIds))
|
|
} else {
|
|
// 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') {
|
|
if (node.scopeIds) {
|
|
node.scopeIds.forEach(id => markKnownIds(id, knownIds))
|
|
} else {
|
|
// #3445 record block-level local variables
|
|
walkBlockDeclarations(node, id =>
|
|
markScopeIdentifier(node, id, knownIds),
|
|
)
|
|
}
|
|
} else if (node.type === 'CatchClause' && node.param) {
|
|
for (const id of extractIdentifiers(node.param)) {
|
|
markScopeIdentifier(node, id, knownIds)
|
|
}
|
|
} else if (isForStatement(node)) {
|
|
walkForStatement(node, false, id =>
|
|
markScopeIdentifier(node, id, knownIds),
|
|
)
|
|
}
|
|
},
|
|
leave(node: Node & { scopeIds?: Set<string> }, parent: Node | null) {
|
|
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[],
|
|
): boolean {
|
|
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 isInNewExpression(parentStack: Node[]): boolean {
|
|
let i = parentStack.length
|
|
while (i--) {
|
|
const p = parentStack[i]
|
|
if (p.type === 'NewExpression') {
|
|
return true
|
|
} else if (p.type !== 'MemberExpression') {
|
|
break
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function walkFunctionParams(
|
|
node: Function,
|
|
onIdent: (id: Identifier) => void,
|
|
): 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,
|
|
): 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)
|
|
} else if (isForStatement(stmt)) {
|
|
walkForStatement(stmt, true, onIdent)
|
|
}
|
|
}
|
|
}
|
|
|
|
function isForStatement(
|
|
stmt: Node,
|
|
): stmt is ForStatement | ForOfStatement | ForInStatement {
|
|
return (
|
|
stmt.type === 'ForOfStatement' ||
|
|
stmt.type === 'ForInStatement' ||
|
|
stmt.type === 'ForStatement'
|
|
)
|
|
}
|
|
|
|
function walkForStatement(
|
|
stmt: ForStatement | ForOfStatement | ForInStatement,
|
|
isVar: boolean,
|
|
onIdent: (id: Identifier) => void,
|
|
) {
|
|
const variable = stmt.type === 'ForStatement' ? stmt.init : stmt.left
|
|
if (
|
|
variable &&
|
|
variable.type === 'VariableDeclaration' &&
|
|
(variable.kind === 'var' ? isVar : !isVar)
|
|
) {
|
|
for (const decl of variable.declarations) {
|
|
for (const id of extractIdentifiers(decl.id)) {
|
|
onIdent(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 markKnownIds(name: string, knownIds: Record<string, number>) {
|
|
if (name in knownIds) {
|
|
knownIds[name]++
|
|
} else {
|
|
knownIds[name] = 1
|
|
}
|
|
}
|
|
|
|
function markScopeIdentifier(
|
|
node: Node & { scopeIds?: Set<string> },
|
|
child: Identifier,
|
|
knownIds: Record<string, number>,
|
|
) {
|
|
const { name } = child
|
|
if (node.scopeIds && node.scopeIds.has(name)) {
|
|
return
|
|
}
|
|
markKnownIds(name, knownIds)
|
|
;(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): boolean =>
|
|
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
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
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 = { someProperty: 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
|
|
}
|
|
|
|
export const TS_NODE_TYPES: string[] = [
|
|
'TSAsExpression', // foo as number
|
|
'TSTypeAssertion', // (<number>foo)
|
|
'TSNonNullExpression', // foo!
|
|
'TSInstantiationExpression', // foo<string>
|
|
'TSSatisfiesExpression', // foo satisfies T
|
|
]
|
|
|
|
export function unwrapTSNode(node: Node): Node {
|
|
if (TS_NODE_TYPES.includes(node.type)) {
|
|
return unwrapTSNode((node as any).expression)
|
|
} else {
|
|
return node
|
|
}
|
|
}
|