]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test: finish tests for transformExpression
authorEvan You <yyx990803@gmail.com>
Tue, 24 Sep 2019 16:12:57 +0000 (12:12 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 24 Sep 2019 16:13:08 +0000 (12:13 -0400)
packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts
packages/compiler-core/src/transforms/transformExpression.ts

index 885bdc01186bff03d1d145c69589e20acad7c56d..ae4cba926c13f2709178b46eeaaadbf0266c8199 100644 (file)
@@ -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(
       `<div>{{ foo }}</div>`
     ) 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(
       `<div v-foo:arg="baz"/>`
     ) 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(
       `<div v-foo:[arg]="baz"/>`
     ) 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(
+      `<div v-for="i in list">{{ i }}{{ j }}</div>`
+    ) 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(
+      `<div v-for="(i, j, k) in list">{{ i + j + k }}{{ l }}</div>`
+    ) 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(
+      `<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('should prefix id outside of v-for', () => {})
+  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` }])
 
-  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' },
+      `]`
+    ])
+  })
 })
index 55c24b533f391d0b22539f221082472f7835f17d..dbb905ae061c19fd3005ad970dda94dd41adf2e2 100644 (file)
@@ -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
     !(