From: Evan You Date: Tue, 24 Sep 2019 16:12:57 +0000 (-0400) Subject: test: finish tests for transformExpression X-Git-Tag: v3.0.0-alpha.0~710 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f5b3f580f1be31ad249a574a439627df8a8eb7e6;p=thirdparty%2Fvuejs%2Fcore.git test: finish tests for transformExpression --- diff --git a/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts b/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts index 885bdc0118..ae4cba926c 100644 --- a/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts @@ -3,7 +3,9 @@ import { transform, ExpressionNode, ElementNode, - DirectiveNode + DirectiveNode, + NodeTypes, + ForNode } from '../../src' import { transformFor } from '../..//src/transforms/vFor' import { transformExpression } from '../../src/transforms/transformExpression' @@ -20,30 +22,63 @@ function parseWithExpressionTransform(template: string) { describe('compiler: expression transform', () => { test('interpolation (root)', () => { const node = parseWithExpressionTransform(`{{ foo }}`) as ExpressionNode - expect(node.content).toBe(`_ctx.foo`) + expect(node.children).toMatchObject([ + `_ctx.`, + { + content: `foo`, + loc: node.loc + } + ]) }) test('interpolation (children)', () => { const node = parseWithExpressionTransform( `
{{ foo }}
` ) as ElementNode - expect((node.children[0] as ExpressionNode).content).toBe(`_ctx.foo`) + expect((node.children[0] as ExpressionNode).children).toMatchObject([ + `_ctx.`, + { + content: `foo`, + loc: node.children[0].loc + } + ]) }) test('directive value', () => { const node = parseWithExpressionTransform( `
` ) as ElementNode - expect((node.props[0] as DirectiveNode).arg!.content).toBe(`arg`) - expect((node.props[0] as DirectiveNode).exp!.content).toBe(`_ctx.baz`) + expect((node.props[0] as DirectiveNode).arg!.children).toBeUndefined() + const exp = (node.props[0] as DirectiveNode).exp! + expect(exp.children).toMatchObject([ + `_ctx.`, + { + content: `baz`, + loc: exp.loc + } + ]) }) test('dynamic directive arg', () => { const node = parseWithExpressionTransform( `
` ) as ElementNode - expect((node.props[0] as DirectiveNode).arg!.content).toBe(`_ctx.arg`) - expect((node.props[0] as DirectiveNode).exp!.content).toBe(`_ctx.baz`) + const arg = (node.props[0] as DirectiveNode).arg! + const exp = (node.props[0] as DirectiveNode).exp! + expect(arg.children).toMatchObject([ + `_ctx.`, + { + content: `arg`, + loc: arg.loc + } + ]) + expect(exp.children).toMatchObject([ + `_ctx.`, + { + content: `baz`, + loc: exp.loc + } + ]) }) test('should prefix complex expressions', () => { @@ -52,42 +87,214 @@ describe('compiler: expression transform', () => { ) as ExpressionNode // should parse into compound expression expect(node.children).toMatchObject([ - { content: `_ctx.foo` }, - `(`, - { content: `_ctx.baz` }, - ` + 1, { key: `, - { content: `_ctx.kuz` }, + `_ctx.`, + { + content: `foo`, + loc: { + source: `foo`, + start: { + offset: 3, + line: 1, + column: 4 + }, + end: { + offset: 6, + line: 1, + column: 7 + } + } + }, + `(_ctx.`, + { + content: `baz`, + loc: { + source: `baz`, + start: { + offset: 7, + line: 1, + column: 8 + }, + end: { + offset: 10, + line: 1, + column: 11 + } + } + }, + ` + 1, { key: _ctx.`, + { + content: `kuz`, + loc: { + source: `kuz`, + start: { + offset: 23, + line: 1, + column: 24 + }, + end: { + offset: 26, + line: 1, + column: 27 + } + } + }, ` })` ]) }) - // TODO FIXME - test('should not prefix v-for aliases', () => { - // const node = parseWithExpressionTransform(`{{ { foo } }}`) as ExpressionNode - // expect(node.children).toMatchObject([ - // `{ foo: `, - // { content: `_ctx.foo` }, - // ` }` - // ]) + test('should not prefix v-for alias', () => { + const node = parseWithExpressionTransform( + `
{{ i }}{{ j }}
` + ) as ForNode + const div = node.children[0] as ElementNode + + const i = div.children[0] as ExpressionNode + expect(i.type).toBe(NodeTypes.EXPRESSION) + expect(i.content).toBe(`i`) + expect(i.children).toBeUndefined() + + const j = div.children[1] as ExpressionNode + expect(j.type).toBe(NodeTypes.EXPRESSION) + expect(j.children).toMatchObject([`_ctx.`, { content: `j` }]) + }) + + test('should not prefix v-for aliases (multiple)', () => { + const node = parseWithExpressionTransform( + `
{{ i + j + k }}{{ l }}
` + ) as ForNode + const div = node.children[0] as ElementNode + + const exp = div.children[0] as ExpressionNode + expect(exp.type).toBe(NodeTypes.EXPRESSION) + expect(exp.content).toBe(`i + j + k`) + expect(exp.children).toBeUndefined() + + const l = div.children[1] as ExpressionNode + expect(l.type).toBe(NodeTypes.EXPRESSION) + expect(l.children).toMatchObject([`_ctx.`, { content: `l` }]) + }) + + test('should prefix id outside of v-for', () => { + const node = parseWithExpressionTransform( + `
{{ i }}
` + ) as ElementNode + const exp = node.children[1] as ExpressionNode + expect(exp.type).toBe(NodeTypes.EXPRESSION) + expect(exp.content).toBe(`i`) + expect(exp.children).toMatchObject([`_ctx.`, { content: `i` }]) }) - test('should prefix id outside of v-for', () => {}) + test('nested v-for', () => { + const node = parseWithExpressionTransform( + `
+
{{ i + j }}
{{ i }} +
` + ) as ForNode + const outerDiv = node.children[0] as ElementNode + const innerFor = outerDiv.children[0] as ForNode + const innerExp = (innerFor.children[0] as ElementNode) + .children[0] as ExpressionNode + expect(innerExp.type).toBe(NodeTypes.EXPRESSION) + expect(innerExp.children).toMatchObject([`i + _ctx.`, { content: `j` }]) - test('nested v-for', () => {}) + // when an inner v-for shadows a variable of an outer v-for and exit, + // it should not cause the outer v-for's alias to be removed from known ids + const outerExp = outerDiv.children[1] as ExpressionNode + expect(outerExp.type).toBe(NodeTypes.EXPRESSION) + expect(outerExp.content).toBe(`i`) + expect(outerExp.children).toBeUndefined() + }) - test('should not prefix whitelisted globals', () => {}) + test('should not prefix whitelisted globals', () => { + const node = parseWithExpressionTransform( + `{{ Math.max(1, 2) }}` + ) as ExpressionNode + expect(node.type).toBe(NodeTypes.EXPRESSION) + expect(node.content).toBe(`Math.max(1, 2)`) + expect(node.children).toBeUndefined() + }) - test('should not prefix id of a function declaration', () => {}) + test('should not prefix id of a function declaration', () => { + const node = parseWithExpressionTransform( + `{{ function foo() { return bar } }}` + ) as ExpressionNode + expect(node.type).toBe(NodeTypes.EXPRESSION) + expect(node.children).toMatchObject([ + `function foo() { return _ctx.`, + { content: `bar` }, + ` }` + ]) + }) test('should not prefix params of a function expression', () => { - // also test object + array destructure + const node = parseWithExpressionTransform( + `{{ foo => foo + bar }}` + ) as ExpressionNode + expect(node.type).toBe(NodeTypes.EXPRESSION) + expect(node.children).toMatchObject([ + `foo => foo + _ctx.`, + { content: `bar` } + ]) + }) + + test('should not prefix an object property key', () => { + const node = parseWithExpressionTransform( + `{{ { foo: bar } }}` + ) as ExpressionNode + expect(node.type).toBe(NodeTypes.EXPRESSION) + expect(node.children).toMatchObject([ + `{ foo: _ctx.`, + { content: `bar` }, + ` }` + ]) }) - test('should not prefix an object property key', () => {}) + test('should prefix a computed object property key', () => { + const node = parseWithExpressionTransform( + `{{ { [foo]: bar } }}` + ) as ExpressionNode + expect(node.type).toBe(NodeTypes.EXPRESSION) + expect(node.children).toMatchObject([ + `{ [_ctx.`, + { content: `foo` }, + `]: _ctx.`, + { content: `bar` }, + ` }` + ]) + }) - test('should prefix a computed object property key', () => {}) + test('should prefix object property shorthand value', () => { + const node = parseWithExpressionTransform(`{{ { foo } }}`) as ExpressionNode + expect(node.children).toMatchObject([ + `{ foo: _ctx.`, + { content: `foo` }, + ` }` + ]) + }) - test('should prefix object property shorthand value', () => {}) + test('should not prefix id in a member expression', () => { + const node = parseWithExpressionTransform( + `{{ foo.bar.baz }}` + ) as ExpressionNode + expect(node.children).toMatchObject([ + `_ctx.`, + { content: `foo` }, + `.bar.baz` + ]) + }) - test('should not prefix id in a member expression', () => {}) + test('should prefix computed id in a member expression', () => { + const node = parseWithExpressionTransform( + `{{ foo[bar][baz] }}` + ) as ExpressionNode + expect(node.children).toMatchObject([ + `_ctx.`, + { content: `foo` }, + `[_ctx.`, + { content: `bar` }, + `][_ctx.`, + { content: 'baz' }, + `]` + ]) + }) }) diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 55c24b533f..dbb905ae06 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -36,10 +36,19 @@ export const transformExpression: NodeTransform = (node, context) => { const simpleIdRE = /^[a-zA-Z$_][\w$]*$/ +const isFunction = (node: Node): node is Function => + /Function(Expression|Declaration)$/.test(node.type) + // cache node requires let _parseScript: typeof parseScript let _walk: typeof walk +interface PrefixMeta { + prefix: string + start: number + end: number +} + // Important: since this function uses Node.js only dependencies, it should // always be used with a leading !__BROWSER__ check so that it can be // tree-shaken from the browser build. @@ -56,7 +65,7 @@ export function processExpression( // fast path if expression is a simple identifier. if (simpleIdRE.test(node.content)) { if (!context.identifiers[node.content]) { - node.content = `_ctx.${node.content}` + node.children = [`_ctx.`, createExpression(node.content, false, node.loc)] } return } @@ -68,21 +77,35 @@ export function processExpression( context.onError(e) return } - const ids: Node[] = [] + + const ids: (Identifier & PrefixMeta)[] = [] const knownIds = Object.create(context.identifiers) + // walk the AST and look for identifiers that need to be prefixed with `_ctx.`. walk(ast, { - enter(node, parent) { + enter(node: Node & PrefixMeta, parent) { if (node.type === 'Identifier') { if ( ids.indexOf(node) === -1 && !knownIds[node.name] && shouldPrefix(node, parent) ) { - node.name = `_ctx.${node.name}` + if ( + parent.type === 'Property' && + parent.value === node && + parent.key === node + ) { + // property shorthand like { foo }, we need to add the key since we + // rewrite the value + node.prefix = `${node.name}: _ctx.` + } else { + node.prefix = `_ctx.` + } ids.push(node) } } else if (isFunction(node)) { + // walk function expressions and add its arguments to known identifiers + // so that we don't prefix them node.params.forEach(p => walk(p, { enter(child) { @@ -107,24 +130,26 @@ export function processExpression( } }) + // We break up the coumpound expression into an array of strings and sub + // expressions (for identifiers that have been prefixed). In codegen, if + // an ExpressionNode has the `.children` property, it will be used instead of + // `.content`. const full = node.content const children: ExpressionNode['children'] = [] - ids.sort((a: any, b: any) => a.start - b.start) - ids.forEach((id: any, i) => { + ids.sort((a, b) => a.start - b.start) + ids.forEach((id, i) => { const last = ids[i - 1] as any - const text = full.slice(last ? last.end - 1 : 0, id.start - 1) - if (text.length) { - children.push(text) - } - const source = full.slice(id.start, id.end) + const leadingText = full.slice(last ? last.end - 1 : 0, id.start - 1) + children.push(leadingText + id.prefix) + const source = full.slice(id.start - 1, id.end - 1) children.push( createExpression(id.name, false, { source, - start: advancePositionWithClone(node.loc.start, source, id.start), - end: advancePositionWithClone(node.loc.start, source, id.end) + start: advancePositionWithClone(node.loc.start, source, id.start + 2), + end: advancePositionWithClone(node.loc.start, source, id.end + 2) }) ) - if (i === ids.length - 1 && id.end < full.length - 1) { + if (i === ids.length - 1 && id.end - 1 < full.length) { children.push(full.slice(id.end - 1)) } }) @@ -145,20 +170,23 @@ const globals = new Set( .split(',') ) -const isFunction = (node: Node): node is Function => - /Function(Expression|Declaration)$/.test(node.type) - function shouldPrefix(identifier: Identifier, parent: Node) { if ( - // not id of a FunctionDeclaration - !(parent.type === 'FunctionDeclaration' && parent.id === identifier) && - // not a params of a function - !(isFunction(parent) && parent.params.indexOf(identifier) > -1) && + !( + isFunction(parent) && + // not id of a FunctionDeclaration + ((parent as any).id === identifier || + // not a params of a function + parent.params.indexOf(identifier) > -1) + ) && // not a key of Property !( parent.type === 'Property' && parent.key === identifier && - !parent.computed + // computed keys should be prefixed + !parent.computed && + // shorthand keys should be prefixed + !(parent.value === identifier) ) && // not a property of a MemberExpression !(