From 60d855e5c5c82b536773ec7d5833a87a54fe727b Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 29 Aug 2024 16:58:35 +0800 Subject: [PATCH] fix(compiler-sfc): throw an error if scope var name conflicts with component name --- .../transformElement.spec.ts.snap | 260 ++++++++++++++++++ .../transforms/transformElement.spec.ts | 41 +++ .../__tests__/transforms/vSlot.spec.ts | 5 +- packages/compiler-core/src/errors.ts | 2 + .../src/transforms/transformElement.ts | 17 ++ packages/compiler-dom/src/errors.ts | 2 +- 6 files changed, 325 insertions(+), 2 deletions(-) diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap index 3da778eb67..1cf0ccae9f 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap @@ -1,5 +1,265 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`compiler: transform component slots > dynamically named slots 1`] = ` +"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, { + [_ctx.one]: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]), + [_ctx.two]: _withCtx(({ bar }) => [_toDisplayString(_ctx.foo), _toDisplayString(bar)]), + _: 2 /* DYNAMIC */ + }, 1024 /* DYNAMIC_SLOTS */)) +}" +`; + +exports[`compiler: transform component slots > implicit default slot 1`] = ` +"const { createElementVNode: _createElementVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, { + default: _withCtx(() => [ + _createElementVNode("div") + ]), + _: 1 /* STABLE */ + })) +}" +`; + +exports[`compiler: transform component slots > named slot with v-for w/ prefixIdentifiers: true 1`] = ` +"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, renderList: _renderList, createSlots: _createSlots, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 2 /* DYNAMIC */ }, [ + _renderList(_ctx.list, (name) => { + return { + name: name, + fn: _withCtx(() => [_toDisplayString(name)]) + } + }) + ]), 1024 /* DYNAMIC_SLOTS */)) +}" +`; + +exports[`compiler: transform component slots > named slot with v-if + prefixIdentifiers: true 1`] = ` +"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 2 /* DYNAMIC */ }, [ + (_ctx.ok) + ? { + name: "one", + fn: _withCtx((props) => [_toDisplayString(props)]), + key: "0" + } + : undefined + ]), 1024 /* DYNAMIC_SLOTS */)) +}" +`; + +exports[`compiler: transform component slots > named slot with v-if + v-else-if + v-else 1`] = ` +"const _Vue = Vue + +return function render(_ctx, _cache) { + with (_ctx) { + const { resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 2 /* DYNAMIC */ }, [ + ok + ? { + name: "one", + fn: _withCtx(() => ["foo"]), + key: "0" + } + : orNot + ? { + name: "two", + fn: _withCtx((props) => ["bar"]), + key: "1" + } + : { + name: "one", + fn: _withCtx(() => ["baz"]), + key: "2" + } + ]), 1024 /* DYNAMIC_SLOTS */)) + } +}" +`; + +exports[`compiler: transform component slots > named slot with v-if 1`] = ` +"const _Vue = Vue + +return function render(_ctx, _cache) { + with (_ctx) { + const { resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 2 /* DYNAMIC */ }, [ + ok + ? { + name: "one", + fn: _withCtx(() => ["hello"]), + key: "0" + } + : undefined + ]), 1024 /* DYNAMIC_SLOTS */)) + } +}" +`; + +exports[`compiler: transform component slots > named slots w/ implicit default slot 1`] = ` +"const _Vue = Vue + +return function render(_ctx, _cache) { + with (_ctx) { + const { createElementVNode: _createElementVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, { + one: _withCtx(() => ["foo"]), + default: _withCtx(() => [ + "bar", + _createElementVNode("span") + ]), + _: 1 /* STABLE */ + })) + } +}" +`; + +exports[`compiler: transform component slots > nested slots scoping 1`] = ` +"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Inner = _resolveComponent("Inner") + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, { + default: _withCtx(({ foo }) => [ + _createVNode(_component_Inner, null, { + default: _withCtx(({ bar }) => [_toDisplayString(foo), _toDisplayString(bar), _toDisplayString(_ctx.baz)]), + _: 2 /* DYNAMIC */ + }, 1024 /* DYNAMIC_SLOTS */), + " ", + _toDisplayString(foo), + _toDisplayString(_ctx.bar), + _toDisplayString(_ctx.baz) + ]), + _: 1 /* STABLE */ + })) +}" +`; + +exports[`compiler: transform component slots > on component dynamically named slot 1`] = ` +"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, { + [_ctx.named]: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]), + _: 2 /* DYNAMIC */ + }, 1024 /* DYNAMIC_SLOTS */)) +}" +`; + +exports[`compiler: transform component slots > on component named slot 1`] = ` +"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, { + named: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]), + _: 1 /* STABLE */ + })) +}" +`; + +exports[`compiler: transform component slots > on-component default slot 1`] = ` +"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, { + default: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]), + _: 1 /* STABLE */ + })) +}" +`; + +exports[`compiler: transform component slots > template named slots 1`] = ` +"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, { + one: _withCtx(({ foo }) => [_toDisplayString(foo), _toDisplayString(_ctx.bar)]), + two: _withCtx(({ bar }) => [_toDisplayString(_ctx.foo), _toDisplayString(bar)]), + _: 1 /* STABLE */ + })) +}" +`; + +exports[`compiler: transform component slots > with whitespace: 'preserve' > implicit default slot 1`] = ` +"const { createElementVNode: _createElementVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, { + header: _withCtx(() => [" Header "]), + default: _withCtx(() => [ + " ", + _createElementVNode("p") + ]), + _: 1 /* STABLE */ + })) +}" +`; + +exports[`compiler: transform component slots > with whitespace: 'preserve' > named default slot + implicit whitespace content 1`] = ` +"const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, { + header: _withCtx(() => [" Header "]), + default: _withCtx(() => [" Default "]), + _: 1 /* STABLE */ + })) +}" +`; + +exports[`compiler: transform component slots > with whitespace: 'preserve' > should not generate whitespace only default slot 1`] = ` +"const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent("Comp") + + return (_openBlock(), _createBlock(_component_Comp, null, { + header: _withCtx(() => [" Header "]), + footer: _withCtx(() => [" Footer "]), + _: 1 /* STABLE */ + })) +}" +`; + exports[`compiler: v-for > codegen > basic v-for 1`] = ` "const _Vue = Vue diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index bf3510a052..c1f67ea4bf 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -40,6 +40,7 @@ import { PatchFlags } from '@vue/shared' import { createObjectMatcher } from '../testUtils' import { transformText } from '../../src/transforms/transformText' import { parseWithForTransform } from './vFor.spec' +import { parseWithSlots } from './vSlot.spec' function parseWithElementTransform( template: string, @@ -1381,4 +1382,44 @@ describe('compiler: element transform', () => { ], }) }) + + test('v-for scope var name conflict with component name', () => { + const onError = vi.fn() + parseWithForTransform(``, { + onError, + prefixIdentifiers: true, + bindingMetadata: { + Comp: BindingTypes.SETUP_CONST, + }, + }) + expect(onError.mock.calls[0]).toMatchObject([ + { + code: ErrorCodes.X_VAR_NAME_CONFLICT_WITH_COMPONENT_NAME, + }, + ]) + }) + + test('slot scope var name conflict with component name', () => { + const onError = vi.fn() + parseWithSlots( + ` + + `, + { + onError, + prefixIdentifiers: true, + bindingMetadata: { + Comp: BindingTypes.SETUP_CONST, + CompB: BindingTypes.SETUP_CONST, + }, + }, + ) + expect(onError.mock.calls[0]).toMatchObject([ + { + code: ErrorCodes.X_VAR_NAME_CONFLICT_WITH_COMPONENT_NAME, + }, + ]) + }) }) diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts index 4766c2ca9d..5a51656c17 100644 --- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts @@ -29,7 +29,10 @@ import { PatchFlags } from '@vue/shared' import { transformFor } from '../../src/transforms/vFor' import { transformIf } from '../../src/transforms/vIf' -function parseWithSlots(template: string, options: CompilerOptions = {}) { +export function parseWithSlots( + template: string, + options: CompilerOptions = {}, +) { const ast = parse(template, { whitespace: options.whitespace, }) diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts index 58e113ab19..c70bc7544b 100644 --- a/packages/compiler-core/src/errors.ts +++ b/packages/compiler-core/src/errors.ts @@ -90,6 +90,7 @@ export enum ErrorCodes { X_V_MODEL_ON_PROPS, X_INVALID_EXPRESSION, X_KEEP_ALIVE_INVALID_CHILDREN, + X_VAR_NAME_CONFLICT_WITH_COMPONENT_NAME, // generic errors X_PREFIX_ID_NOT_SUPPORTED, @@ -179,6 +180,7 @@ export const errorMessages: Record = { [ErrorCodes.X_INVALID_EXPRESSION]: `Error parsing JavaScript expression: `, [ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: ` expects exactly one child component.`, [ErrorCodes.X_VNODE_HOOKS]: `@vnode-* hooks in templates are no longer supported. Use the vue: prefix instead. For example, @vnode-mounted should be changed to @vue:mounted. @vnode-* hooks support has been removed in 3.4.`, + [ErrorCodes.X_VAR_NAME_CONFLICT_WITH_COMPONENT_NAME]: `the variable name conflicts with the component name.`, // generic errors [ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`, diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index c917436ea9..47b3c92c69 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -319,6 +319,20 @@ export function resolveComponentType( return toValidAssetId(tag, `component`) } +/** + * dev only + */ +const checkName = (name: string, context: TransformContext) => { + if (context.identifiers[name]) { + context.onError( + createCompilerError( + ErrorCodes.X_VAR_NAME_CONFLICT_WITH_COMPONENT_NAME, + context.currentNode!.loc, + ), + ) + } +} + function resolveSetupReference(name: string, context: TransformContext) { const bindings = context.bindingMetadata if (!bindings || bindings.__isScriptSetup === false) { @@ -344,6 +358,7 @@ function resolveSetupReference(name: string, context: TransformContext) { checkType(BindingTypes.SETUP_REACTIVE_CONST) || checkType(BindingTypes.LITERAL_CONST) if (fromConst) { + __DEV__ && checkName(fromConst, context) return context.inline ? // in inline mode, const setup bindings (e.g. imports) can be used as-is fromConst @@ -355,6 +370,7 @@ function resolveSetupReference(name: string, context: TransformContext) { checkType(BindingTypes.SETUP_REF) || checkType(BindingTypes.SETUP_MAYBE_REF) if (fromMaybeRef) { + __DEV__ && checkName(fromMaybeRef, context) return context.inline ? // setup scope bindings that may be refs need to be unrefed `${context.helperString(UNREF)}(${fromMaybeRef})` @@ -363,6 +379,7 @@ function resolveSetupReference(name: string, context: TransformContext) { const fromProps = checkType(BindingTypes.PROPS) if (fromProps) { + __DEV__ && checkName(fromProps, context) return `${context.helperString(UNREF)}(${ context.inline ? '__props' : '$props' }[${JSON.stringify(fromProps)}])` diff --git a/packages/compiler-dom/src/errors.ts b/packages/compiler-dom/src/errors.ts index b47624840a..5bffa4e53a 100644 --- a/packages/compiler-dom/src/errors.ts +++ b/packages/compiler-dom/src/errors.ts @@ -21,7 +21,7 @@ export function createDOMCompilerError( } export enum DOMErrorCodes { - X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */, + X_V_HTML_NO_EXPRESSION = 54 /* ErrorCodes.__EXTEND_POINT__ */, X_V_HTML_WITH_CHILDREN, X_V_TEXT_NO_EXPRESSION, X_V_TEXT_WITH_CHILDREN, -- 2.47.2