From 73f8cae4650199c3be06e2f2c9a9e85cf66e4e34 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 22 Aug 2021 14:51:16 -0400 Subject: [PATCH] refactor(compiler): further extract babel ast utilities --- packages/compiler-core/src/babelUtils.ts | 231 ++++++++++++++++++ packages/compiler-core/src/index.ts | 1 + .../src/transforms/transformExpression.ts | 132 ++++------ packages/compiler-sfc/src/compileScript.ts | 168 +------------ packages/compiler-sfc/src/index.ts | 3 +- packages/shared/src/astUtils.ts | 72 ------ packages/shared/src/index.ts | 1 - 7 files changed, 284 insertions(+), 324 deletions(-) create mode 100644 packages/compiler-core/src/babelUtils.ts delete mode 100644 packages/shared/src/astUtils.ts diff --git a/packages/compiler-core/src/babelUtils.ts b/packages/compiler-core/src/babelUtils.ts new file mode 100644 index 000000000..49b3d1a3c --- /dev/null +++ b/packages/compiler-core/src/babelUtils.ts @@ -0,0 +1,231 @@ +import { + Identifier, + Node, + isReferenced, + Function, + ObjectProperty +} 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, + onNode?: (node: Node, parent: Node, parentStack: Node[]) => void | boolean, + parentStack: Node[] = [], + knownIds: Record = Object.create(null), + includeAll = false +) { + const rootExp = + root.type === 'Program' && + root.body[0].type === 'ExpressionStatement' && + root.body[0].expression + + ;(walk as any)(root, { + enter(node: Node & { scopeIds?: Set }, 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 (onNode && onNode(node, parent!, parentStack) === false) { + 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 (isFunctionType(node)) { + // walk function expressions and add its arguments to known identifiers + // so that we don't prefix them + for (const p of node.params) { + ;(walk as any)(p, { + enter(child: Node, parent: Node) { + if ( + child.type === 'Identifier' && + // do not record as scope variable if is a destructured key + !isStaticPropertyKey(child, parent) && + // do not record if this is a default value + // assignment of a destructured variable + !( + parent && + parent.type === 'AssignmentPattern' && + parent.right === child + ) + ) { + markScopeIdentifier(node, child, knownIds) + } + } + }) + } + } else if (node.type === 'BlockStatement') { + // #3445 record block-level local variables + for (const stmt of node.body) { + if (stmt.type === 'VariableDeclaration') { + for (const decl of stmt.declarations) { + for (const id of extractIdentifiers(decl.id)) { + markScopeIdentifier(node, id, knownIds) + } + } + } + } + } else if ( + node.type === 'ObjectProperty' && + parent!.type === 'ObjectPattern' + ) { + // mark property in destructure pattern + ;(node as any).inPattern = true + } + }, + leave(node: Node & { scopeIds?: Set }, parent: Node | undefined) { + parent && parentStack.pop() + if (node !== rootExp && node.scopeIds) { + node.scopeIds.forEach((id: string) => { + knownIds[id]-- + if (knownIds[id] === 0) { + delete knownIds[id] + } + }) + } + } + }) +} + +export function isReferencedIdentifier( + id: Identifier, + parent: Node | null, + parentStack: Node[] +) { + 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 +} + +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': + param.properties.forEach(prop => { + 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 }, + child: Identifier, + knownIds: Record +) { + 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 diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 303de3af7..1233e0ada 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -31,6 +31,7 @@ export { export * from './ast' export * from './utils' +export * from './babelUtils' export * from './runtimeHelpers' export { getBaseTransformPreset, TransformPreset } from './compile' diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 4efd6d4aa..4c30707fc 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -17,18 +17,19 @@ import { createCompoundExpression, ConstantTypes } from '../ast' +import { + isInDestructureAssignment, + isStaticProperty, + isStaticPropertyKey, + walkIdentifiers +} from '../babelUtils' import { advancePositionWithClone, isSimpleIdentifier } from '../utils' import { isGloballyWhitelisted, makeMap, babelParserDefaultPlugins, hasOwn, - isString, - isReferencedIdentifier, - isInDestructureAssignment, - isStaticProperty, - isStaticPropertyKey, - isFunctionType + isString } from '@vue/shared' import { createCompilerError, ErrorCodes } from '../errors' import { @@ -39,7 +40,6 @@ import { } from '@babel/types' import { validateBrowserExpression } from '../validateExpression' import { parse } from '@babel/parser' -import { walk } from 'estree-walker' import { IS_REF, UNREF } from '../runtimeHelpers' import { BindingTypes } from '../options' @@ -245,89 +245,49 @@ export function processExpression( return node } - const ids: (Identifier & PrefixMeta)[] = [] - const knownIds = Object.create(context.identifiers) - const isDuplicate = (node: Node & PrefixMeta): boolean => - ids.some(id => id.start === node.start) + type QualifiedId = Identifier & PrefixMeta + + const ids: QualifiedId[] = [] const parentStack: Node[] = [] + const knownIds: Record = Object.create(context.identifiers) - // walk the AST and look for identifiers that need to be prefixed. - ;(walk as any)(ast, { - enter(node: Node & PrefixMeta, parent: Node | undefined) { - parent && parentStack.push(parent) - if (node.type === 'Identifier') { - if (!isDuplicate(node)) { - // v2 wrapped filter call - if (__COMPAT__ && node.name.startsWith('_filter_')) { - return - } + walkIdentifiers( + ast, + (node, parent, _, isReferenced, isLocal) => { + if (isStaticPropertyKey(node, parent!)) { + return + } + // v2 wrapped filter call + if (__COMPAT__ && node.name.startsWith('_filter_')) { + return + } - const needPrefix = shouldPrefix(node, parent!, parentStack) - if (!knownIds[node.name] && needPrefix) { - if (isStaticProperty(parent!) && parent.shorthand) { - // property shorthand like { foo }, we need to add the key since - // we rewrite the value - node.prefix = `${node.name}: ` - } - node.name = rewriteIdentifier(node.name, parent, node) - ids.push(node) - } else if (!isStaticPropertyKey(node, parent!)) { - // The identifier is considered constant unless it's pointing to a - // scope variable (a v-for alias, or a v-slot prop) - if (!(needPrefix && knownIds[node.name]) && !bailConstant) { - node.isConstant = true - } - // also generate sub-expressions for other identifiers for better - // source map support. (except for property keys which are static) - ids.push(node) - } + const needPrefix = isReferenced && canPrefix(node) + if (needPrefix && !isLocal) { + if (isStaticProperty(parent!) && parent.shorthand) { + // property shorthand like { foo }, we need to add the key since + // we rewrite the value + ;(node as QualifiedId).prefix = `${node.name}: ` } - } else if (isFunctionType(node)) { - // walk function expressions and add its arguments to known identifiers - // so that we don't prefix them - node.params.forEach(p => - (walk as any)(p, { - enter(child: Node, parent: Node) { - if ( - child.type === 'Identifier' && - // do not record as scope variable if is a destructured key - !isStaticPropertyKey(child, parent) && - // do not record if this is a default value - // assignment of a destructured variable - !( - parent && - parent.type === 'AssignmentPattern' && - parent.right === child - ) - ) { - 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) - } - } - }) - ) + node.name = rewriteIdentifier(node.name, parent, node) + ids.push(node as QualifiedId) + } else { + // The identifier is considered constant unless it's pointing to a + // local scope variable (a v-for alias, or a v-slot prop) + if (!(needPrefix && isLocal) && !bailConstant) { + ;(node as QualifiedId).isConstant = true + } + // also generate sub-expressions for other identifiers for better + // source map support. (except for property keys which are static) + ids.push(node as QualifiedId) } }, - leave(node: Node & PrefixMeta, parent: Node | undefined) { - parent && parentStack.pop() - if (node !== ast.body[0].expression && node.scopeIds) { - node.scopeIds.forEach((id: string) => { - knownIds[id]-- - if (knownIds[id] === 0) { - delete knownIds[id] - } - }) - } - } - }) + undefined, + parentStack, + knownIds, + // invoke on ALL identifiers + true + ) // We break up the compound expression into an array of strings and sub // expressions (for identifiers that have been prefixed). In codegen, if @@ -375,7 +335,7 @@ export function processExpression( return ret } -function shouldPrefix(id: Identifier, parent: Node, parentStack: Node[]) { +function canPrefix(id: Identifier) { // skip whitelisted globals if (isGloballyWhitelisted(id.name)) { return false @@ -384,7 +344,7 @@ function shouldPrefix(id: Identifier, parent: Node, parentStack: Node[]) { if (id.name === 'require') { return false } - return isReferencedIdentifier(id, parent, parentStack) + return true } function stringifyExpression(exp: ExpressionNode | string): string { diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 1e560fe8d..4ed0b2a61 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -8,7 +8,10 @@ import { transform, parserOptions, UNREF, - SimpleExpressionNode + SimpleExpressionNode, + isFunctionType, + isStaticProperty, + walkIdentifiers } from '@vue/compiler-dom' import { ScriptSetupTextRanges, @@ -22,10 +25,6 @@ import { camelize, capitalize, generateCodeFrame, - isFunctionType, - isReferencedIdentifier, - isStaticProperty, - isStaticPropertyKey, makeMap } from '@vue/shared' import { @@ -1154,9 +1153,7 @@ export function compileScript( } for (const node of scriptSetupAst) { - if (node.type !== 'ImportDeclaration') { - walkIdentifiers(node, onIdent, onNode) - } + walkIdentifiers(node, onIdent, onNode) } } @@ -1774,116 +1771,6 @@ function genRuntimeEmits(emits: Set) { : `` } -function markScopeIdentifier( - node: Node & { scopeIds?: Set }, - child: Identifier, - knownIds: Record -) { - 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) -} - -/** - * Walk an AST and find identifiers that are variable references. - * This is largely the same logic with `transformExpressions` in compiler-core - * but with some subtle differences as this needs to handle a wider range of - * possible syntax. - */ -export function walkIdentifiers( - root: Node, - onIdentifier: (node: Identifier, parent: Node, parentStack: Node[]) => void, - onNode?: (node: Node, parent: Node, parentStack: Node[]) => void | boolean -) { - const parentStack: Node[] = [] - const knownIds: Record = Object.create(null) - ;(walk as any)(root, { - enter(node: Node & { scopeIds?: Set }, 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 (onNode && onNode(node, parent!, parentStack) === false) { - return this.skip() - } - if (node.type === 'Identifier') { - if ( - !knownIds[node.name] && - isReferencedIdentifier(node, parent!, parentStack) - ) { - onIdentifier(node, parent!, parentStack) - } - } else if (isFunctionType(node)) { - // #3445 - // should not rewrite local variables sharing a name with a top-level ref - if (node.body.type === 'BlockStatement') { - node.body.body.forEach(p => { - if (p.type === 'VariableDeclaration') { - for (const decl of p.declarations) { - extractIdentifiers(decl.id).forEach(id => { - markScopeIdentifier(node, id, knownIds) - }) - } - } - }) - } - // walk function expressions and add its arguments to known identifiers - // so that we don't prefix them - node.params.forEach(p => - (walk as any)(p, { - enter(child: Node, parent: Node) { - if ( - child.type === 'Identifier' && - // do not record as scope variable if is a destructured key - !isStaticPropertyKey(child, parent) && - // do not record if this is a default value - // assignment of a destructured variable - !( - parent && - parent.type === 'AssignmentPattern' && - parent.right === child - ) - ) { - markScopeIdentifier(node, child, knownIds) - } - } - }) - ) - } else if ( - node.type === 'ObjectProperty' && - parent!.type === 'ObjectPattern' - ) { - // mark property in destructure pattern - ;(node as any).inPattern = true - } - }, - leave(node: Node & { scopeIds?: Set }, parent: Node | undefined) { - parent && parentStack.pop() - if (node.scopeIds) { - node.scopeIds.forEach((id: string) => { - knownIds[id]-- - if (knownIds[id] === 0) { - delete knownIds[id] - } - }) - } - } - }) -} - function isCallOf( node: Node | null | undefined, test: string | ((id: string) => boolean) @@ -2077,51 +1964,6 @@ function getObjectOrArrayExpressionKeys(value: Node): string[] { return [] } -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': - param.properties.forEach(prop => { - 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 toTextRange(node: Node): TextRange { return { start: node.start!, diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts index 9acc94466..9d9383260 100644 --- a/packages/compiler-sfc/src/index.ts +++ b/packages/compiler-sfc/src/index.ts @@ -4,11 +4,10 @@ export { compileTemplate } from './compileTemplate' export { compileStyle, compileStyleAsync } from './compileStyle' export { compileScript } from './compileScript' export { rewriteDefault } from './rewriteDefault' -export { generateCodeFrame } from '@vue/compiler-core' +export { generateCodeFrame, walkIdentifiers } from '@vue/compiler-core' // Utilities export { parse as babelParse } from '@babel/parser' -export { walkIdentifiers } from './compileScript' import MagicString from 'magic-string' export { MagicString } export { walk } from 'estree-walker' diff --git a/packages/shared/src/astUtils.ts b/packages/shared/src/astUtils.ts deleted file mode 100644 index cb01c8e9a..000000000 --- a/packages/shared/src/astUtils.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - Identifier, - Node, - isReferenced, - Function, - ObjectProperty -} from '@babel/types' - -export function isReferencedIdentifier( - id: Identifier, - parent: Node | null, - parentStack: Node[] -) { - 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 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 diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 0bd2df438..8c98b3b6c 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -12,7 +12,6 @@ export * from './domAttrConfig' export * from './escapeHtml' export * from './looseEqual' export * from './toDisplayString' -export * from './astUtils' /** * List of @babel/parser plugins that are used for template expression