]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-sfc): analyze import usage in template via AST (#9729)
authorEvan You <yyx990803@gmail.com>
Fri, 1 Dec 2023 03:14:48 +0000 (11:14 +0800)
committerGitHub <noreply@github.com>
Fri, 1 Dec 2023 03:14:48 +0000 (11:14 +0800)
close #8897
close nuxt/nuxt#22416

12 files changed:
packages/compiler-core/__tests__/parse.spec.ts
packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/babelUtils.ts
packages/compiler-core/src/options.ts
packages/compiler-core/src/parser.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/__tests__/utils.ts
packages/compiler-sfc/src/parse.ts
packages/compiler-sfc/src/script/importUsageCheck.ts

index 05a2afcdc6eb3c46c08268b48241cb6f26c29cd5..9d84e80c2a6e031a9866419e19259bfd5a23b82c 100644 (file)
@@ -14,6 +14,7 @@ import {
 } from '../src/ast'
 
 import { baseParse } from '../src/parser'
+import { Program } from '@babel/types'
 
 /* eslint jest/no-disabled-tests: "off" */
 
@@ -2170,6 +2171,63 @@ describe('compiler: parse', () => {
     })
   })
 
+  describe('expression parsing', () => {
+    test('interpolation', () => {
+      const ast = baseParse(`{{ a + b }}`, { prefixIdentifiers: true })
+      // @ts-ignore
+      expect((ast.children[0] as InterpolationNode).content.ast?.type).toBe(
+        'BinaryExpression'
+      )
+    })
+
+    test('v-bind', () => {
+      const ast = baseParse(`<div :[key+1]="foo()" />`, {
+        prefixIdentifiers: true
+      })
+      const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
+      // @ts-ignore
+      expect(dir.arg?.ast?.type).toBe('BinaryExpression')
+      // @ts-ignore
+      expect(dir.exp?.ast?.type).toBe('CallExpression')
+    })
+
+    test('v-on multi statements', () => {
+      const ast = baseParse(`<div @click="a++;b++" />`, {
+        prefixIdentifiers: true
+      })
+      const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
+      // @ts-ignore
+      expect(dir.exp?.ast?.type).toBe('Program')
+      expect((dir.exp?.ast as Program).body).toMatchObject([
+        { type: 'ExpressionStatement' },
+        { type: 'ExpressionStatement' }
+      ])
+    })
+
+    test('v-slot', () => {
+      const ast = baseParse(`<Comp #foo="{ a, b }" />`, {
+        prefixIdentifiers: true
+      })
+      const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
+      // @ts-ignore
+      expect(dir.exp?.ast?.type).toBe('ArrowFunctionExpression')
+    })
+
+    test('v-for', () => {
+      const ast = baseParse(`<div v-for="({ a, b }, key, index) of a.b" />`, {
+        prefixIdentifiers: true
+      })
+      const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
+      const { source, value, key, index } = dir.forParseResult!
+      // @ts-ignore
+      expect(source.ast?.type).toBe('MemberExpression')
+      // @ts-ignore
+      expect(value?.ast?.type).toBe('ArrowFunctionExpression')
+      expect(key?.ast).toBeNull() // simple ident
+      expect(index?.ast).toBeNull() // simple ident
+    })
+  })
+
   describe('Errors', () => {
     // HTML parsing errors as specified at
     // https://html.spec.whatwg.org/multipage/parsing.html#parse-errors
index a9697930c95dce6d0ce058d598753a3f1fd284d4..b33cbbd80f6ef9ed9e6a82315caff5e61f84ef33 100644 (file)
@@ -18,7 +18,7 @@ function parseWithExpressionTransform(
   template: string,
   options: CompilerOptions = {}
 ) {
-  const ast = parse(template)
+  const ast = parse(template, options)
   transform(ast, {
     prefixIdentifiers: true,
     nodeTransforms: [transformIf, transformExpression],
index 2bc85bf53d8775187b8be5d38843451d20dece9c..203fa8b2c6b0c1b32b0ab9905d10a1d002150fdf 100644 (file)
@@ -14,6 +14,7 @@ import {
 } from './runtimeHelpers'
 import { PropsExpression } from './transforms/transformElement'
 import { ImportItem, TransformContext } from './transform'
+import { Node as BabelNode } from '@babel/types'
 
 // Vue template is a platform-agnostic superset of HTML (syntax only).
 // More namespaces can be declared by platform specific compilers.
@@ -226,6 +227,12 @@ export interface SimpleExpressionNode extends Node {
   content: string
   isStatic: boolean
   constType: ConstantTypes
+  /**
+   * - `null` means the expression is a simple identifier that doesn't need
+   *    parsing
+   * - `false` means there was a parsing error
+   */
+  ast?: BabelNode | null | false
   /**
    * Indicates this is an identifier for a hoist vnode call and points to the
    * hoisted node.
@@ -246,6 +253,12 @@ export interface InterpolationNode extends Node {
 
 export interface CompoundExpressionNode extends Node {
   type: NodeTypes.COMPOUND_EXPRESSION
+  /**
+   * - `null` means the expression is a simple identifier that doesn't need
+   *    parsing
+   * - `false` means there was a parsing error
+   */
+  ast?: BabelNode | null | false
   children: (
     | SimpleExpressionNode
     | CompoundExpressionNode
index 1f1e3896a1e07acf7cb45ebfbce86f77ef94c957..f3ef5df29db4fa7a6a367c1814c26a98964cbf02 100644 (file)
@@ -28,9 +28,9 @@ export function walkIdentifiers(
   }
 
   const rootExp =
-    root.type === 'Program' &&
-    root.body[0].type === 'ExpressionStatement' &&
-    root.body[0].expression
+    root.type === 'Program'
+      ? root.body[0].type === 'ExpressionStatement' && root.body[0].expression
+      : root
 
   walk(root, {
     enter(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
index e0c4099e40ea7fc20e5a5c8d3577cfe5c60c27a6..5710039ca10b08ebbca5a5d6edcea4b17e54665c 100644 (file)
@@ -86,6 +86,17 @@ export interface ParserOptions
    * This defaults to `true` in development and `false` in production builds.
    */
   comments?: boolean
+  /**
+   * Parse JavaScript expressions with Babel.
+   * @default false
+   */
+  prefixIdentifiers?: boolean
+  /**
+   * A list of parser plugins to enable for `@babel/parser`, which is used to
+   * parse expressions in bindings and interpolations.
+   * https://babeljs.io/docs/en/next/babel-parser#plugins
+   */
+  expressionPlugins?: ParserPlugin[]
 }
 
 export type HoistTransform = (
index f4399d7c67f116e9101f299fb87e62e4772dc2d2..f1d712b3643c8715673baad2216b153fde9d4ca8 100644 (file)
@@ -38,14 +38,25 @@ import {
   defaultOnError,
   defaultOnWarn
 } from './errors'
-import { forAliasRE, isCoreComponent, isStaticArgOf } from './utils'
+import {
+  forAliasRE,
+  isCoreComponent,
+  isSimpleIdentifier,
+  isStaticArgOf
+} from './utils'
 import { decodeHTML } from 'entities/lib/decode.js'
+import {
+  parse,
+  parseExpression,
+  type ParserOptions as BabelOptions
+} from '@babel/parser'
 
 type OptionalOptions =
   | 'decodeEntities'
   | 'whitespace'
   | 'isNativeTag'
   | 'isBuiltInComponent'
+  | 'expressionPlugins'
   | keyof CompilerCompatOptions
 
 export type MergedParserOptions = Omit<
@@ -64,7 +75,8 @@ export const defaultParserOptions: MergedParserOptions = {
   isCustomElement: NO,
   onError: defaultOnError,
   onWarn: defaultOnWarn,
-  comments: __DEV__
+  comments: __DEV__,
+  prefixIdentifiers: false
 }
 
 let currentOptions: MergedParserOptions = defaultParserOptions
@@ -116,7 +128,7 @@ const tokenizer = new Tokenizer(stack, {
     }
     addNode({
       type: NodeTypes.INTERPOLATION,
-      content: createSimpleExpression(exp, false, getLoc(innerStart, innerEnd)),
+      content: createExp(exp, false, getLoc(innerStart, innerEnd)),
       loc: getLoc(start, end)
     })
   },
@@ -245,7 +257,7 @@ const tokenizer = new Tokenizer(stack, {
       setLocEnd((currentProp as AttributeNode).nameLoc, end)
     } else {
       const isStatic = arg[0] !== `[`
-      ;(currentProp as DirectiveNode).arg = createSimpleExpression(
+      ;(currentProp as DirectiveNode).arg = createExp(
         isStatic ? arg : arg.slice(1, -1),
         isStatic,
         getLoc(start, end),
@@ -346,10 +358,25 @@ const tokenizer = new Tokenizer(stack, {
           }
         } else {
           // directive
-          currentProp.exp = createSimpleExpression(
+          let expParseMode = ExpParseMode.Normal
+          if (!__BROWSER__) {
+            if (currentProp.name === 'for') {
+              expParseMode = ExpParseMode.Skip
+            } else if (currentProp.name === 'slot') {
+              expParseMode = ExpParseMode.Params
+            } else if (
+              currentProp.name === 'on' &&
+              currentAttrValue.includes(';')
+            ) {
+              expParseMode = ExpParseMode.Statements
+            }
+          }
+          currentProp.exp = createExp(
             currentAttrValue,
             false,
-            getLoc(currentAttrStartIndex, currentAttrEndIndex)
+            getLoc(currentAttrStartIndex, currentAttrEndIndex),
+            ConstantTypes.NOT_CONSTANT,
+            expParseMode
           )
           if (currentProp.name === 'for') {
             currentProp.forParseResult = parseForExpression(currentProp.exp)
@@ -477,10 +504,20 @@ function parseForExpression(
 
   const [, LHS, RHS] = inMatch
 
-  const createAliasExpression = (content: string, offset: number) => {
+  const createAliasExpression = (
+    content: string,
+    offset: number,
+    asParam = false
+  ) => {
     const start = loc.start.offset + offset
     const end = start + content.length
-    return createSimpleExpression(content, false, getLoc(start, end))
+    return createExp(
+      content,
+      false,
+      getLoc(start, end),
+      ConstantTypes.NOT_CONSTANT,
+      asParam ? ExpParseMode.Params : ExpParseMode.Normal
+    )
   }
 
   const result: ForParseResult = {
@@ -502,7 +539,7 @@ function parseForExpression(
     let keyOffset: number | undefined
     if (keyContent) {
       keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
-      result.key = createAliasExpression(keyContent, keyOffset)
+      result.key = createAliasExpression(keyContent, keyOffset, true)
     }
 
     if (iteratorMatch[2]) {
@@ -516,14 +553,15 @@ function parseForExpression(
             result.key
               ? keyOffset! + keyContent.length
               : trimmedOffset + valueContent.length
-          )
+          ),
+          true
         )
       }
     }
   }
 
   if (valueContent) {
-    result.value = createAliasExpression(valueContent, trimmedOffset)
+    result.value = createAliasExpression(valueContent, trimmedOffset, true)
   }
 
   return result
@@ -929,8 +967,58 @@ function dirToAttr(dir: DirectiveNode): AttributeNode {
   return attr
 }
 
-function emitError(code: ErrorCodes, index: number) {
-  currentOptions.onError(createCompilerError(code, getLoc(index, index)))
+enum ExpParseMode {
+  Normal,
+  Params,
+  Statements,
+  Skip
+}
+
+function createExp(
+  content: SimpleExpressionNode['content'],
+  isStatic: SimpleExpressionNode['isStatic'] = false,
+  loc: SourceLocation,
+  constType: ConstantTypes = ConstantTypes.NOT_CONSTANT,
+  parseMode = ExpParseMode.Normal
+) {
+  const exp = createSimpleExpression(content, isStatic, loc, constType)
+  if (
+    !__BROWSER__ &&
+    !isStatic &&
+    currentOptions.prefixIdentifiers &&
+    parseMode !== ExpParseMode.Skip &&
+    content.trim()
+  ) {
+    if (isSimpleIdentifier(content)) {
+      exp.ast = null // fast path
+      return exp
+    }
+    try {
+      const plugins = currentOptions.expressionPlugins
+      const options: BabelOptions = {
+        plugins: plugins ? [...plugins, 'typescript'] : ['typescript']
+      }
+      if (parseMode === ExpParseMode.Statements) {
+        // v-on with multi-inline-statements, pad 1 char
+        exp.ast = parse(` ${content} `, options).program
+      } else if (parseMode === ExpParseMode.Params) {
+        exp.ast = parseExpression(`(${content})=>{}`, options)
+      } else {
+        // normal exp, wrap with parens
+        exp.ast = parseExpression(`(${content})`, options)
+      }
+    } catch (e: any) {
+      exp.ast = false // indicate an error
+      emitError(ErrorCodes.X_INVALID_EXPRESSION, loc.start.offset, e.message)
+    }
+  }
+  return exp
+}
+
+function emitError(code: ErrorCodes, index: number, message?: string) {
+  currentOptions.onError(
+    createCompilerError(code, getLoc(index, index), undefined, message)
+  )
 }
 
 function reset() {
index 4d9a2497886d259419b604adf52b1e3086ba743a..263ada4f1378c59c66c0edccc7bff0797b6f4b76 100644 (file)
@@ -223,7 +223,14 @@ export function processExpression(
   // bail constant on parens (function invocation) and dot (member access)
   const bailConstant = constantBailRE.test(rawExp)
 
-  if (isSimpleIdentifier(rawExp)) {
+  let ast = node.ast
+
+  if (ast === false) {
+    // ast being false means it has caused an error already during parse phase
+    return node
+  }
+
+  if (ast === null || (!ast && isSimpleIdentifier(rawExp))) {
     const isScopeVarReference = context.identifiers[rawExp]
     const isAllowedGlobal = isGloballyAllowed(rawExp)
     const isLiteral = isLiteralWhitelisted(rawExp)
@@ -249,29 +256,30 @@ export function processExpression(
     return node
   }
 
-  let ast: any
-  // exp needs to be parsed differently:
-  // 1. Multiple inline statements (v-on, with presence of `;`): parse as raw
-  //    exp, but make sure to pad with spaces for consistent ranges
-  // 2. Expressions: wrap with parens (for e.g. object expressions)
-  // 3. Function arguments (v-for, v-slot): place in a function argument position
-  const source = asRawStatements
-    ? ` ${rawExp} `
-    : `(${rawExp})${asParams ? `=>{}` : ``}`
-  try {
-    ast = parse(source, {
-      plugins: context.expressionPlugins
-    }).program
-  } catch (e: any) {
-    context.onError(
-      createCompilerError(
-        ErrorCodes.X_INVALID_EXPRESSION,
-        node.loc,
-        undefined,
-        e.message
+  if (!ast) {
+    // exp needs to be parsed differently:
+    // 1. Multiple inline statements (v-on, with presence of `;`): parse as raw
+    //    exp, but make sure to pad with spaces for consistent ranges
+    // 2. Expressions: wrap with parens (for e.g. object expressions)
+    // 3. Function arguments (v-for, v-slot): place in a function argument position
+    const source = asRawStatements
+      ? ` ${rawExp} `
+      : `(${rawExp})${asParams ? `=>{}` : ``}`
+    try {
+      ast = parse(source, {
+        plugins: context.expressionPlugins
+      }).program
+    } catch (e: any) {
+      context.onError(
+        createCompilerError(
+          ErrorCodes.X_INVALID_EXPRESSION,
+          node.loc,
+          undefined,
+          e.message
+        )
       )
-    )
-    return node
+      return node
+    }
   }
 
   type QualifiedId = Identifier & PrefixMeta
@@ -351,6 +359,7 @@ export function processExpression(
   let ret
   if (children.length) {
     ret = createCompoundExpression(children, node.loc)
+    ret.ast = ast
   } else {
     ret = node
     ret.constType = bailConstant
index 4cc3cf611d8734b6fabf122b2b613037973ed56e..e26dfef53d90e38c11f3866c78c19a6df5333586 100644 (file)
@@ -748,6 +748,51 @@ return { get FooBaz() { return FooBaz }, get Last() { return Last } }
 })"
 `;
 
+exports[`SFC compile <script setup> > dev mode import usage check > property access (whitespace) 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+import { Foo, Bar, Baz } from './foo'
+        
+export default /*#__PURE__*/_defineComponent({
+  setup(__props, { expose: __expose }) {
+  __expose();
+
+          
+return { get Foo() { return Foo } }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> > dev mode import usage check > property access 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+import { Foo, Bar, Baz } from './foo'
+        
+export default /*#__PURE__*/_defineComponent({
+  setup(__props, { expose: __expose }) {
+  __expose();
+
+          
+return { get Foo() { return Foo } }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> > dev mode import usage check > spread operator 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+import { Foo, Bar, Baz } from './foo'
+        
+export default /*#__PURE__*/_defineComponent({
+  setup(__props, { expose: __expose }) {
+  __expose();
+
+          
+return { get Foo() { return Foo } }
+}
+
+})"
+`;
+
 exports[`SFC compile <script setup> > dev mode import usage check > template ref 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
 import { foo, bar, Baz } from './foo'
index 5632f570984176a87d9d9bd743930d93f4c11f9e..0b551af1a5097a7f86ff1527db7c095aa19a4daf 100644 (file)
@@ -243,7 +243,7 @@ describe('SFC compile <script setup>', () => {
       import { useCssVars, ref } from 'vue'
       const msg = ref()
       </script>
-      
+
       <style>
       .foo {
         color: v-bind(msg)
@@ -518,6 +518,46 @@ describe('SFC compile <script setup>', () => {
       )
       assertCode(content)
     })
+
+    // https://github.com/nuxt/nuxt/issues/22416
+    test('property access', () => {
+      const { content } = compile(`
+        <script setup lang="ts">
+          import { Foo, Bar, Baz } from './foo'
+        </script>
+        <template>
+          <div>{{ Foo.Bar.Baz }}</div>
+        </template>
+        `)
+      expect(content).toMatch('return { get Foo() { return Foo } }')
+      assertCode(content)
+    })
+
+    test('spread operator', () => {
+      const { content } = compile(`
+        <script setup lang="ts">
+          import { Foo, Bar, Baz } from './foo'
+        </script>
+        <template>
+          <div v-bind="{ ...Foo.Bar.Baz }"></div>
+        </template>
+        `)
+      expect(content).toMatch('return { get Foo() { return Foo } }')
+      assertCode(content)
+    })
+
+    test('property access (whitespace)', () => {
+      const { content } = compile(`
+        <script setup lang="ts">
+          import { Foo, Bar, Baz } from './foo'
+        </script>
+        <template>
+          <div>{{ Foo . Bar . Baz }}</div>
+        </template>
+        `)
+      expect(content).toMatch('return { get Foo() { return Foo } }')
+      assertCode(content)
+    })
   })
 
   describe('inlineTemplate mode', () => {
index ffa12652c83293f4fbe34db591c569064d96a377..13e12d765a21caeb3e4467009c3d8a20e26876f3 100644 (file)
@@ -13,7 +13,10 @@ export function compileSFCScript(
   options?: Partial<SFCScriptCompileOptions>,
   parseOptions?: SFCParseOptions
 ) {
-  const { descriptor } = parse(src, parseOptions)
+  const { descriptor, errors } = parse(src, parseOptions)
+  if (errors.length) {
+    console.warn(errors[0])
+  }
   return compileScript(descriptor, {
     ...options,
     id: mockId
index b7d9ee652b871047ff10c2d20089819c36132b39..84e7c0d066b93b2caf6fcf85584893fb6b69a1aa 100644 (file)
@@ -24,6 +24,7 @@ export interface SFCParseOptions {
   pad?: boolean | 'line' | 'space'
   ignoreEmpty?: boolean
   compiler?: TemplateCompiler
+  parseExpressions?: boolean
 }
 
 export interface SFCBlock {
@@ -104,7 +105,8 @@ export function parse(
     sourceRoot = '',
     pad = false,
     ignoreEmpty = true,
-    compiler = CompilerDOM
+    compiler = CompilerDOM,
+    parseExpressions = true
   }: SFCParseOptions = {}
 ): SFCParseResult {
   const sourceKey =
@@ -130,6 +132,7 @@ export function parse(
   const errors: (CompilerError | SyntaxError)[] = []
   const ast = compiler.parse(source, {
     parseMode: 'sfc',
+    prefixIdentifiers: parseExpressions,
     onError: e => {
       errors.push(e)
     }
index cd97566d55f2dddbd719a70d72b85786d59b8b24..f9fcca235d963fd1aa10d7c932641bfafed9d947 100644 (file)
@@ -1,12 +1,11 @@
-import { parseExpression } from '@babel/parser'
 import { SFCDescriptor } from '../parse'
 import {
   NodeTypes,
   SimpleExpressionNode,
-  forAliasRE,
   parserOptions,
   walkIdentifiers,
-  TemplateChildNode
+  TemplateChildNode,
+  ExpressionNode
 } from '@vue/compiler-dom'
 import { createCache } from '../cache'
 import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
@@ -17,14 +16,10 @@ import { camelize, capitalize, isBuiltInDirective } from '@vue/shared'
  * when not using inline mode.
  */
 export function isImportUsed(local: string, sfc: SFCDescriptor): boolean {
-  return new RegExp(
-    // #4274 escape $ since it's a special char in regex
-    // (and is the only regex special char that is valid in identifiers)
-    `[^\\w$_]${local.replace(/\$/g, '\\$')}[^\\w$_]`
-  ).test(resolveTemplateUsageCheckString(sfc))
+  return resolveTemplateUsageCheckString(sfc).has(local)
 }
 
-const templateUsageCheckCache = createCache<string>()
+const templateUsageCheckCache = createCache<Set<string>>()
 
 function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
   const { content, ast } = sfc.template!
@@ -33,7 +28,7 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
     return cached
   }
 
-  let code = ''
+  const ids = new Set<string>()
 
   ast!.children.forEach(walk)
 
@@ -44,27 +39,25 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
           !parserOptions.isNativeTag!(node.tag) &&
           !parserOptions.isBuiltInComponent!(node.tag)
         ) {
-          code += `,${camelize(node.tag)},${capitalize(camelize(node.tag))}`
+          ids.add(camelize(node.tag))
+          ids.add(capitalize(camelize(node.tag)))
         }
         for (let i = 0; i < node.props.length; i++) {
           const prop = node.props[i]
           if (prop.type === NodeTypes.DIRECTIVE) {
             if (!isBuiltInDirective(prop.name)) {
-              code += `,v${capitalize(camelize(prop.name))}`
+              ids.add(`v${capitalize(camelize(prop.name))}`)
             }
 
             // process dynamic directive arguments
             if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) {
-              code += `,${stripStrings(
-                (prop.arg as SimpleExpressionNode).content
-              )}`
+              extractIdentifiers(ids, prop.arg)
             }
 
-            if (prop.exp) {
-              code += `,${processExp(
-                (prop.exp as SimpleExpressionNode).content,
-                prop.name
-              )}`
+            if (prop.name === 'for') {
+              extractIdentifiers(ids, prop.forParseResult!.source)
+            } else if (prop.exp) {
+              extractIdentifiers(ids, prop.exp)
             }
           }
           if (
@@ -72,58 +65,25 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
             prop.name === 'ref' &&
             prop.value?.content
           ) {
-            code += `,${prop.value.content}`
+            ids.add(prop.value.content)
           }
         }
         node.children.forEach(walk)
         break
       case NodeTypes.INTERPOLATION:
-        code += `,${processExp((node.content as SimpleExpressionNode).content)}`
+        extractIdentifiers(ids, node.content)
         break
     }
   }
 
-  code += ';'
-  templateUsageCheckCache.set(content, code)
-  return code
+  templateUsageCheckCache.set(content, ids)
+  return ids
 }
 
-function processExp(exp: string, dir?: string): string {
-  if (/ as\s+\w|<.*>|:/.test(exp)) {
-    if (dir === 'slot') {
-      exp = `(${exp})=>{}`
-    } else if (dir === 'on') {
-      exp = `()=>{return ${exp}}`
-    } else if (dir === 'for') {
-      const inMatch = exp.match(forAliasRE)
-      if (inMatch) {
-        let [, LHS, RHS] = inMatch
-        // #6088
-        LHS = LHS.trim().replace(/^\(|\)$/g, '')
-        return processExp(`(${LHS})=>{}`) + processExp(RHS)
-      }
-    }
-    let ret = ''
-    // has potential type cast or generic arguments that uses types
-    const ast = parseExpression(exp, { plugins: ['typescript'] })
-    walkIdentifiers(ast, node => {
-      ret += `,` + node.name
-    })
-    return ret
-  }
-  return stripStrings(exp)
-}
-
-function stripStrings(exp: string) {
-  return exp
-    .replace(/'[^']*'|"[^"]*"/g, '')
-    .replace(/`[^`]+`/g, stripTemplateString)
-}
-
-function stripTemplateString(str: string): string {
-  const interpMatch = str.match(/\${[^}]+}/g)
-  if (interpMatch) {
-    return interpMatch.map(m => m.slice(2, -1)).join(',')
+function extractIdentifiers(ids: Set<string>, node: ExpressionNode) {
+  if (node.ast) {
+    walkIdentifiers(node.ast, n => ids.add(n.name))
+  } else if (node.ast === null) {
+    ids.add((node as SimpleExpressionNode).content)
   }
-  return ''
 }