test: finish tests for transformExpression

This commit is contained in:
Evan You 2019-09-24 12:12:57 -04:00
parent 4a82e7cdbc
commit f5b3f580f1
2 changed files with 286 additions and 51 deletions

View File

@ -3,7 +3,9 @@ import {
transform, transform,
ExpressionNode, ExpressionNode,
ElementNode, ElementNode,
DirectiveNode DirectiveNode,
NodeTypes,
ForNode
} from '../../src' } from '../../src'
import { transformFor } from '../..//src/transforms/vFor' import { transformFor } from '../..//src/transforms/vFor'
import { transformExpression } from '../../src/transforms/transformExpression' import { transformExpression } from '../../src/transforms/transformExpression'
@ -20,30 +22,63 @@ function parseWithExpressionTransform(template: string) {
describe('compiler: expression transform', () => { describe('compiler: expression transform', () => {
test('interpolation (root)', () => { test('interpolation (root)', () => {
const node = parseWithExpressionTransform(`{{ foo }}`) as ExpressionNode 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)', () => { test('interpolation (children)', () => {
const node = parseWithExpressionTransform( const node = parseWithExpressionTransform(
`<div>{{ foo }}</div>` `<div>{{ foo }}</div>`
) as ElementNode ) 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', () => { test('directive value', () => {
const node = parseWithExpressionTransform( const node = parseWithExpressionTransform(
`<div v-foo:arg="baz"/>` `<div v-foo:arg="baz"/>`
) as ElementNode ) as ElementNode
expect((node.props[0] as DirectiveNode).arg!.content).toBe(`arg`) expect((node.props[0] as DirectiveNode).arg!.children).toBeUndefined()
expect((node.props[0] as DirectiveNode).exp!.content).toBe(`_ctx.baz`) const exp = (node.props[0] as DirectiveNode).exp!
expect(exp.children).toMatchObject([
`_ctx.`,
{
content: `baz`,
loc: exp.loc
}
])
}) })
test('dynamic directive arg', () => { test('dynamic directive arg', () => {
const node = parseWithExpressionTransform( const node = parseWithExpressionTransform(
`<div v-foo:[arg]="baz"/>` `<div v-foo:[arg]="baz"/>`
) as ElementNode ) as ElementNode
expect((node.props[0] as DirectiveNode).arg!.content).toBe(`_ctx.arg`) const arg = (node.props[0] as DirectiveNode).arg!
expect((node.props[0] as DirectiveNode).exp!.content).toBe(`_ctx.baz`) 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', () => { test('should prefix complex expressions', () => {
@ -52,42 +87,214 @@ describe('compiler: expression transform', () => {
) as ExpressionNode ) as ExpressionNode
// should parse into compound expression // should parse into compound expression
expect(node.children).toMatchObject([ expect(node.children).toMatchObject([
{ content: `_ctx.foo` }, `_ctx.`,
`(`, {
{ content: `_ctx.baz` }, content: `foo`,
` + 1, { key: `, loc: {
{ content: `_ctx.kuz` }, 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 alias', () => {
test('should not prefix v-for aliases', () => { const node = parseWithExpressionTransform(
// const node = parseWithExpressionTransform(`{{ { foo } }}`) as ExpressionNode `<div v-for="i in list">{{ i }}{{ j }}</div>`
// expect(node.children).toMatchObject([ ) as ForNode
// `{ foo: `, const div = node.children[0] as ElementNode
// { content: `_ctx.foo` },
// ` }` 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 prefix id outside of v-for', () => {}) test('should not prefix v-for aliases (multiple)', () => {
const node = parseWithExpressionTransform(
`<div v-for="(i, j, k) in list">{{ i + j + k }}{{ l }}</div>`
) as ForNode
const div = node.children[0] as ElementNode
test('nested v-for', () => {}) const exp = div.children[0] as ExpressionNode
expect(exp.type).toBe(NodeTypes.EXPRESSION)
expect(exp.content).toBe(`i + j + k`)
expect(exp.children).toBeUndefined()
test('should not prefix whitelisted globals', () => {}) const l = div.children[1] as ExpressionNode
expect(l.type).toBe(NodeTypes.EXPRESSION)
expect(l.children).toMatchObject([`_ctx.`, { content: `l` }])
})
test('should not prefix id of a function declaration', () => {}) test('should prefix id outside of v-for', () => {
const node = parseWithExpressionTransform(
`<div><div v-for="i in list" />{{ i }}</div>`
) 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('nested v-for', () => {
const node = parseWithExpressionTransform(
`<div v-for="i in list">
<div v-for="i in list">{{ i + j }}</div>{{ i }}
</div>`
) 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` }])
// 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', () => {
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', () => {
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', () => { 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', () => {}) 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 prefix a computed 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 object property shorthand value', () => {}) test('should prefix object property shorthand value', () => {
const node = parseWithExpressionTransform(`{{ { foo } }}`) as ExpressionNode
expect(node.children).toMatchObject([
`{ foo: _ctx.`,
{ content: `foo` },
` }`
])
})
test('should not prefix id in a member expression', () => {}) 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 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' },
`]`
])
})
}) })

View File

@ -36,10 +36,19 @@ export const transformExpression: NodeTransform = (node, context) => {
const simpleIdRE = /^[a-zA-Z$_][\w$]*$/ const simpleIdRE = /^[a-zA-Z$_][\w$]*$/
const isFunction = (node: Node): node is Function =>
/Function(Expression|Declaration)$/.test(node.type)
// cache node requires // cache node requires
let _parseScript: typeof parseScript let _parseScript: typeof parseScript
let _walk: typeof walk let _walk: typeof walk
interface PrefixMeta {
prefix: string
start: number
end: number
}
// Important: since this function uses Node.js only dependencies, it should // Important: since this function uses Node.js only dependencies, it should
// always be used with a leading !__BROWSER__ check so that it can be // always be used with a leading !__BROWSER__ check so that it can be
// tree-shaken from the browser build. // tree-shaken from the browser build.
@ -56,7 +65,7 @@ export function processExpression(
// fast path if expression is a simple identifier. // fast path if expression is a simple identifier.
if (simpleIdRE.test(node.content)) { if (simpleIdRE.test(node.content)) {
if (!context.identifiers[node.content]) { if (!context.identifiers[node.content]) {
node.content = `_ctx.${node.content}` node.children = [`_ctx.`, createExpression(node.content, false, node.loc)]
} }
return return
} }
@ -68,21 +77,35 @@ export function processExpression(
context.onError(e) context.onError(e)
return return
} }
const ids: Node[] = []
const ids: (Identifier & PrefixMeta)[] = []
const knownIds = Object.create(context.identifiers) const knownIds = Object.create(context.identifiers)
// walk the AST and look for identifiers that need to be prefixed with `_ctx.`.
walk(ast, { walk(ast, {
enter(node, parent) { enter(node: Node & PrefixMeta, parent) {
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
if ( if (
ids.indexOf(node) === -1 && ids.indexOf(node) === -1 &&
!knownIds[node.name] && !knownIds[node.name] &&
shouldPrefix(node, parent) 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) ids.push(node)
} }
} else if (isFunction(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 => node.params.forEach(p =>
walk(p, { walk(p, {
enter(child) { 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 full = node.content
const children: ExpressionNode['children'] = [] const children: ExpressionNode['children'] = []
ids.sort((a: any, b: any) => a.start - b.start) ids.sort((a, b) => a.start - b.start)
ids.forEach((id: any, i) => { ids.forEach((id, i) => {
const last = ids[i - 1] as any const last = ids[i - 1] as any
const text = full.slice(last ? last.end - 1 : 0, id.start - 1) const leadingText = full.slice(last ? last.end - 1 : 0, id.start - 1)
if (text.length) { children.push(leadingText + id.prefix)
children.push(text) const source = full.slice(id.start - 1, id.end - 1)
}
const source = full.slice(id.start, id.end)
children.push( children.push(
createExpression(id.name, false, { createExpression(id.name, false, {
source, source,
start: advancePositionWithClone(node.loc.start, source, id.start), start: advancePositionWithClone(node.loc.start, source, id.start + 2),
end: advancePositionWithClone(node.loc.start, source, id.end) 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)) children.push(full.slice(id.end - 1))
} }
}) })
@ -145,20 +170,23 @@ const globals = new Set(
.split(',') .split(',')
) )
const isFunction = (node: Node): node is Function =>
/Function(Expression|Declaration)$/.test(node.type)
function shouldPrefix(identifier: Identifier, parent: Node) { function shouldPrefix(identifier: Identifier, parent: Node) {
if ( if (
!(
isFunction(parent) &&
// not id of a FunctionDeclaration // not id of a FunctionDeclaration
!(parent.type === 'FunctionDeclaration' && parent.id === identifier) && ((parent as any).id === identifier ||
// not a params of a function // not a params of a function
!(isFunction(parent) && parent.params.indexOf(identifier) > -1) && parent.params.indexOf(identifier) > -1)
) &&
// not a key of Property // not a key of Property
!( !(
parent.type === 'Property' && parent.type === 'Property' &&
parent.key === identifier && 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 // not a property of a MemberExpression
!( !(