Skip to content

Commit

Permalink
fix(compiler-sfc): use ast for import usage check
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Nov 30, 2023
1 parent 4011d3b commit ef4ad2b
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 65 deletions.
6 changes: 6 additions & 0 deletions packages/compiler-core/src/parser.ts
Expand Up @@ -990,6 +990,12 @@ function createExp(
const options = {
plugins: currentOptions.expressionPlugins
}
if (/ as\s+\w|<.*>|:/.test(content)) {
options.plugins = [
...(currentOptions.expressionPlugins || []),
'typescript'
]
}
if (parseMode === ExpParseMode.Statements) {
// v-on with multi-inline-statements, pad 1 char
exp.ast = parse(` ${content} `, options).program
Expand Down
Expand Up @@ -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'
Expand Down
42 changes: 41 additions & 1 deletion packages/compiler-sfc/__tests__/compileScript.spec.ts
Expand Up @@ -243,7 +243,7 @@ describe('SFC compile <script setup>', () => {
import { useCssVars, ref } from 'vue'
const msg = ref()
</script>
<style>
.foo {
color: v-bind(msg)
Expand Down Expand Up @@ -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', () => {
Expand Down
5 changes: 4 additions & 1 deletion packages/compiler-sfc/__tests__/utils.ts
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion packages/compiler-sfc/src/parse.ts
Expand Up @@ -24,6 +24,7 @@ export interface SFCParseOptions {
pad?: boolean | 'line' | 'space'
ignoreEmpty?: boolean
compiler?: TemplateCompiler
parseExpressions?: boolean
}

export interface SFCBlock {
Expand Down Expand Up @@ -104,7 +105,8 @@ export function parse(
sourceRoot = '',
pad = false,
ignoreEmpty = true,
compiler = CompilerDOM
compiler = CompilerDOM,
parseExpressions = true
}: SFCParseOptions = {}
): SFCParseResult {
const sourceKey =
Expand All @@ -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)
}
Expand Down
84 changes: 22 additions & 62 deletions packages/compiler-sfc/src/script/importUsageCheck.ts
@@ -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'
Expand All @@ -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!
Expand All @@ -33,7 +28,7 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
return cached
}

let code = ''
const ids = new Set<string>()

ast!.children.forEach(walk)

Expand All @@ -44,86 +39,51 @@ 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 (
prop.type === NodeTypes.ATTRIBUTE &&
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) {
ids.add((node as SimpleExpressionNode).content)
return
}
return ''
walkIdentifiers(node.ast, n => ids.add(n.name))
}

0 comments on commit ef4ad2b

Please sign in to comment.