]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-core/compiler-sfc): handle destructure assignment expressions
authorEvan You <yyx990803@gmail.com>
Thu, 19 Nov 2020 03:39:08 +0000 (22:39 -0500)
committerEvan You <yyx990803@gmail.com>
Thu, 19 Nov 2020 03:39:08 +0000 (22:39 -0500)
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/template-explorer/src/options.ts

index 4ad7d8884a6029a551b951c63f9ed0e9c48a8d3e..83f80e87553765022cac2c85f05e98fad7ffc4c3 100644 (file)
@@ -111,22 +111,26 @@ export function processExpression(
   const rewriteIdentifier = (raw: string, parent?: Node, id?: Identifier) => {
     const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
     if (inline) {
+      // x = y
       const isAssignmentLVal =
         parent && parent.type === 'AssignmentExpression' && parent.left === id
+      // x++
       const isUpdateArg =
         parent && parent.type === 'UpdateExpression' && parent.argument === id
-      // setup inline mode
+      // ({ x } = y)
+      const isDestructureAssignment =
+        parent && isInDestructureAssignment(parent, parentStack)
+
       if (type === BindingTypes.SETUP_CONST) {
         return raw
-      } else if (
-        type === BindingTypes.SETUP_REF ||
-        type === BindingTypes.SETUP_MAYBE_REF
-      ) {
+      } else if (type === BindingTypes.SETUP_REF) {
+        return `${raw}.value`
+      } else if (type === BindingTypes.SETUP_MAYBE_REF) {
         // const binding that may or may not be ref
         // if it's not a ref, then assignments don't make sense -
         // so we ignore the non-ref assignment case and generate code
         // that assumes the value to be a ref for more efficiency
-        return isAssignmentLVal || isUpdateArg
+        return isAssignmentLVal || isUpdateArg || isDestructureAssignment
           ? `${raw}.value`
           : `${context.helperString(UNREF)}(${raw})`
       } else if (type === BindingTypes.SETUP_LET) {
@@ -157,6 +161,13 @@ export function processExpression(
           return `${context.helperString(IS_REF)}(${raw})${
             context.isTS ? ` //@ts-ignore\n` : ``
           } ? ${prefix}${raw}.value${postfix} : ${prefix}${raw}${postfix}`
+        } else if (isDestructureAssignment) {
+          // TODO
+          // let binding in a destructure assignment - it's very tricky to
+          // handle both possible cases here without altering the original
+          // structure of the code, so we just assume it's not a ref here
+          // for now
+          return raw
         } else {
           return `${context.helperString(UNREF)}(${raw})`
         }
@@ -231,22 +242,24 @@ export function processExpression(
   const knownIds = Object.create(context.identifiers)
   const isDuplicate = (node: Node & PrefixMeta): boolean =>
     ids.some(id => id.start === node.start)
+  const parentStack: Node[] = []
 
   // walk the AST and look for identifiers that need to be prefixed.
   ;(walk as any)(ast, {
-    enter(node: Node & PrefixMeta, parent: Node) {
+    enter(node: Node & PrefixMeta, parent: Node | undefined) {
+      parent && parentStack.push(parent)
       if (node.type === 'Identifier') {
         if (!isDuplicate(node)) {
-          const needPrefix = shouldPrefix(node, parent)
+          const needPrefix = shouldPrefix(node, parent!, parentStack)
           if (!knownIds[node.name] && needPrefix) {
-            if (isStaticProperty(parent) && parent.shorthand) {
+            if (isStaticProperty(parent!) && parent.shorthand) {
               // property shorthand like { foo }, we need to add the key since
               // we rewrite the value
               node.prefix = `${node.name}: `
             }
             node.name = rewriteIdentifier(node.name, parent, node)
             ids.push(node)
-          } else if (!isStaticPropertyKey(node, parent)) {
+          } else if (!isStaticPropertyKey(node, parent!)) {
             // The identifier is considered constant unless it's pointing to a
             // scope variable (a v-for alias, or a v-slot prop)
             if (!(needPrefix && knownIds[node.name]) && !bailConstant) {
@@ -291,7 +304,8 @@ export function processExpression(
         )
       }
     },
-    leave(node: Node & PrefixMeta) {
+    leave(node: Node & PrefixMeta, parent: Node | undefined) {
+      parent && parentStack.pop()
       if (node !== ast.body[0].expression && node.scopeIds) {
         node.scopeIds.forEach((id: string) => {
           knownIds[id]--
@@ -359,7 +373,7 @@ const isStaticProperty = (node: Node): node is ObjectProperty =>
 const isStaticPropertyKey = (node: Node, parent: Node) =>
   isStaticProperty(parent) && parent.key === node
 
-function shouldPrefix(id: Identifier, parent: Node) {
+function shouldPrefix(id: Identifier, parent: Node, parentStack: Node[]) {
   // declaration id
   if (
     (parent.type === 'VariableDeclarator' ||
@@ -386,8 +400,11 @@ function shouldPrefix(id: Identifier, parent: Node) {
     return false
   }
 
-  // array destructure pattern
-  if (parent.type === 'ArrayPattern') {
+  // non-assignment array destructure pattern
+  if (
+    parent.type === 'ArrayPattern' &&
+    !isInDestructureAssignment(parent, parentStack)
+  ) {
     return false
   }
 
@@ -419,6 +436,24 @@ function shouldPrefix(id: Identifier, parent: Node) {
   return true
 }
 
+function isInDestructureAssignment(parent: Node, parentStack: Node[]): boolean {
+  if (
+    parent &&
+    (parent.type === 'ObjectProperty' || parent.type === 'ArrayPattern')
+  ) {
+    let i = parentStack.length
+    while (i--) {
+      const p = parentStack[i]
+      if (p.type === 'AssignmentExpression') {
+        return true
+      } else if (p.type !== 'ObjectProperty' && !p.type.endsWith('Pattern')) {
+        break
+      }
+    }
+  }
+  return false
+}
+
 function stringifyExpression(exp: ExpressionNode | string): string {
   if (isString(exp)) {
     return exp
index a08802a8c58eb0e3d545e6557065a56f6b38ea0b..6ff7608ccfe1f6255a93c2c2b5548ec4479bf993 100644 (file)
@@ -117,7 +117,7 @@ return { ref }
 `;
 
 exports[`SFC compile <script setup> inlineTemplate mode avoid unref() when necessary 1`] = `
-"import { createVNode as _createVNode, unref as _unref, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
+"import { createVNode as _createVNode, toDisplayString as _toDisplayString, unref as _unref, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
 
 import { ref } from 'vue'
         import Foo from './Foo.vue'
@@ -129,12 +129,14 @@ export default {
 
         const count = ref(0)
         const constant = {}
+        const maybe = foo()
+        let lett = 1
         function fn() {}
         
 return (_ctx, _cache) => {
   return (_openBlock(), _createBlock(_Fragment, null, [
     _createVNode(Foo),
-    _createVNode(\\"div\\", { onClick: fn }, _toDisplayString(_unref(count)) + \\" \\" + _toDisplayString(constant) + \\" \\" + _toDisplayString(_unref(other)), 1 /* TEXT */)
+    _createVNode(\\"div\\", { onClick: fn }, _toDisplayString(count.value) + \\" \\" + _toDisplayString(constant) + \\" \\" + _toDisplayString(_unref(maybe)) + \\" \\" + _toDisplayString(_unref(lett)) + \\" \\" + _toDisplayString(_unref(other)), 1 /* TEXT */)
   ], 64 /* STABLE_FRAGMENT */))
 }
 }
@@ -143,7 +145,7 @@ return (_ctx, _cache) => {
 `;
 
 exports[`SFC compile <script setup> inlineTemplate mode should work 1`] = `
-"import { unref as _unref, toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
+"import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
 
 const _hoisted_1 = /*#__PURE__*/_createVNode(\\"div\\", null, \\"static\\", -1 /* HOISTED */)
 
@@ -157,7 +159,7 @@ export default {
         
 return (_ctx, _cache) => {
   return (_openBlock(), _createBlock(_Fragment, null, [
-    _createVNode(\\"div\\", null, _toDisplayString(_unref(count)), 1 /* TEXT */),
+    _createVNode(\\"div\\", null, _toDisplayString(count.value), 1 /* TEXT */),
     _hoisted_1
   ], 64 /* STABLE_FRAGMENT */))
 }
@@ -167,7 +169,7 @@ return (_ctx, _cache) => {
 `;
 
 exports[`SFC compile <script setup> inlineTemplate mode template assignment expression codegen 1`] = `
-"import { createVNode as _createVNode, isRef as _isRef, unref as _unref, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
+"import { createVNode as _createVNode, isRef as _isRef, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
 
 import { ref } from 'vue'
         
@@ -185,10 +187,42 @@ return (_ctx, _cache) => {
       onClick: _cache[1] || (_cache[1] = $event => (count.value = 1))
     }),
     _createVNode(\\"div\\", {
-      onClick: _cache[2] || (_cache[2] = $event => (!_isRef(maybe) ? null : maybe.value = _unref(count)))
+      onClick: _cache[2] || (_cache[2] = $event => (maybe.value = count.value))
+    }),
+    _createVNode(\\"div\\", {
+      onClick: _cache[3] || (_cache[3] = $event => (_isRef(lett) ? lett.value = count.value : lett = count.value))
+    })
+  ], 64 /* STABLE_FRAGMENT */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> inlineTemplate mode template destructure assignment codegen 1`] = `
+"import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
+
+import { ref } from 'vue'
+        
+export default {
+  expose: [],
+  setup(__props) {
+
+        const val = {}
+        const count = ref(0)
+        const maybe = foo()
+        let lett = 1
+        
+return (_ctx, _cache) => {
+  return (_openBlock(), _createBlock(_Fragment, null, [
+    _createVNode(\\"div\\", {
+      onClick: _cache[1] || (_cache[1] = $event => (({ count: count.value } = val)))
+    }),
+    _createVNode(\\"div\\", {
+      onClick: _cache[2] || (_cache[2] = $event => ([maybe.value] = val))
     }),
     _createVNode(\\"div\\", {
-      onClick: _cache[3] || (_cache[3] = $event => (_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)))
+      onClick: _cache[3] || (_cache[3] = $event => (({ lett: lett } = val)))
     })
   ], 64 /* STABLE_FRAGMENT */))
 }
@@ -219,10 +253,10 @@ return (_ctx, _cache) => {
       onClick: _cache[2] || (_cache[2] = $event => (--count.value))
     }),
     _createVNode(\\"div\\", {
-      onClick: _cache[3] || (_cache[3] = $event => (!_isRef(maybe) ? null : maybe.value++))
+      onClick: _cache[3] || (_cache[3] = $event => (maybe.value++))
     }),
     _createVNode(\\"div\\", {
-      onClick: _cache[4] || (_cache[4] = $event => (!_isRef(maybe) ? null : --maybe.value))
+      onClick: _cache[4] || (_cache[4] = $event => (--maybe.value))
     }),
     _createVNode(\\"div\\", {
       onClick: _cache[5] || (_cache[5] = $event => (_isRef(lett) ? lett.value++ : lett++))
@@ -238,7 +272,7 @@ return (_ctx, _cache) => {
 `;
 
 exports[`SFC compile <script setup> inlineTemplate mode v-model codegen 1`] = `
-"import { unref as _unref, vModelText as _vModelText, createVNode as _createVNode, withDirectives as _withDirectives, isRef as _isRef, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
+"import { vModelText as _vModelText, createVNode as _createVNode, withDirectives as _withDirectives, unref as _unref, isRef as _isRef, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
 
 import { ref } from 'vue'
         
@@ -255,7 +289,7 @@ return (_ctx, _cache) => {
     _withDirectives(_createVNode(\\"input\\", {
       \\"onUpdate:modelValue\\": _cache[1] || (_cache[1] = $event => (count.value = $event))
     }, null, 512 /* NEED_PATCH */), [
-      [_vModelText, _unref(count)]
+      [_vModelText, count.value]
     ]),
     _withDirectives(_createVNode(\\"input\\", {
       \\"onUpdate:modelValue\\": _cache[2] || (_cache[2] = $event => (_isRef(maybe) ? maybe.value = $event : null))
@@ -364,6 +398,8 @@ export default {
         a.value = a.value + 1
         b.value.count++
         b.value.count = b.value.count + 1
+        ;({ a: a.value } = { a: 2 })
+        ;[a.value] = [1]
       }
       
 return { a, b, inc }
index a610d3be5fb1193282d3c8bb7d12b4726c72179b..0123a9da37fc5bab67aaa0cb10e15444524f5d92 100644 (file)
@@ -128,28 +128,34 @@ const bar = 1
         import other from './util'
         const count = ref(0)
         const constant = {}
+        const maybe = foo()
+        let lett = 1
         function fn() {}
         </script>
         <template>
           <Foo/>
-          <div @click="fn">{{ count }} {{ constant }} {{ other }}</div>
+          <div @click="fn">{{ count }} {{ constant }} {{ maybe }} {{ lett }} {{ other }}</div>
         </template>
         `,
         { inlineTemplate: true }
       )
-      assertCode(content)
       // no need to unref vue component import
       expect(content).toMatch(`createVNode(Foo)`)
       // should unref other imports
       expect(content).toMatch(`unref(other)`)
       // no need to unref constant literals
       expect(content).not.toMatch(`unref(constant)`)
-      // should unref const w/ call init (e.g. ref())
-      expect(content).toMatch(`unref(count)`)
+      // should directly use .value for known refs
+      expect(content).toMatch(`count.value`)
+      // should unref() on const bindings that may be refs
+      expect(content).toMatch(`unref(maybe)`)
+      // should unref() on let bindings
+      expect(content).toMatch(`unref(lett)`)
       // no need to unref function declarations
       expect(content).toMatch(`{ onClick: fn }`)
       // no need to mark constant fns in patch flag
       expect(content).not.toMatch(`PROPS`)
+      assertCode(content)
     })
 
     test('v-model codegen', () => {
@@ -170,8 +176,9 @@ const bar = 1
       )
       // known const ref: set value
       expect(content).toMatch(`count.value = $event`)
-      // const but maybe ref: only assign after check
-      expect(content).toMatch(`_isRef(maybe) ? maybe.value = $event : null`)
+      // const but maybe ref: also assign .value directly since non-ref
+      // won't work
+      expect(content).toMatch(`maybe.value = $event`)
       // let: handle both cases
       expect(content).toMatch(
         `_isRef(lett) ? lett.value = $event : lett = $event`
@@ -198,12 +205,10 @@ const bar = 1
       // known const ref: set value
       expect(content).toMatch(`count.value = 1`)
       // const but maybe ref: only assign after check
-      expect(content).toMatch(
-        `!_isRef(maybe) ? null : maybe.value = _unref(count)`
-      )
+      expect(content).toMatch(`maybe.value = count.value`)
       // let: handle both cases
       expect(content).toMatch(
-        `_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)`
+        `_isRef(lett) ? lett.value = count.value : lett = count.value`
       )
       assertCode(content)
     })
@@ -230,14 +235,40 @@ const bar = 1
       // known const ref: set value
       expect(content).toMatch(`count.value++`)
       expect(content).toMatch(`--count.value`)
-      // const but maybe ref: only assign after check
-      expect(content).toMatch(`!_isRef(maybe) ? null : maybe.value++`)
-      expect(content).toMatch(`!_isRef(maybe) ? null : --maybe.value`)
+      // const but maybe ref (non-ref case ignored)
+      expect(content).toMatch(`maybe.value++`)
+      expect(content).toMatch(`--maybe.value`)
       // let: handle both cases
       expect(content).toMatch(`_isRef(lett) ? lett.value++ : lett++`)
       expect(content).toMatch(`_isRef(lett) ? --lett.value : --lett`)
       assertCode(content)
     })
+
+    test('template destructure assignment codegen', () => {
+      const { content } = compile(
+        `<script setup>
+        import { ref } from 'vue'
+        const val = {}
+        const count = ref(0)
+        const maybe = foo()
+        let lett = 1
+        </script>
+        <template>
+          <div @click="({ count } = val)"/>
+          <div @click="[maybe] = val"/>
+          <div @click="({ lett } = val)"/>
+        </template>
+        `,
+        { inlineTemplate: true }
+      )
+      // known const ref: set value
+      expect(content).toMatch(`({ count: count.value } = val)`)
+      // const but maybe ref (non-ref case ignored)
+      expect(content).toMatch(`[maybe.value] = val`)
+      // let: assumes non-ref
+      expect(content).toMatch(`{ lett: lett } = val`)
+      assertCode(content)
+    })
   })
 
   describe('with TypeScript', () => {
@@ -524,12 +555,16 @@ const { props, emit } = defineOptions({
         a = a + 1
         b.count++
         b.count = b.count + 1
+        ;({ a } = { a: 2 })
+        ;[a] = [1]
       }
       </script>`)
       expect(content).toMatch(`a.value++`)
       expect(content).toMatch(`a.value = a.value + 1`)
       expect(content).toMatch(`b.value.count++`)
       expect(content).toMatch(`b.value.count = b.value.count + 1`)
+      expect(content).toMatch(`;({ a: a.value } = { a: 2 })`)
+      expect(content).toMatch(`;[a.value] = [1]`)
       assertCode(content)
     })
 
index 4e1d1288df2ac026b44afa510cf2a16919560114..78579be5efcc09a6cbe8adbf0141ee30739fb42c 100644 (file)
@@ -670,7 +670,10 @@ export function compileScript(
               // let binding used in a property shorthand
               // { foo } -> { foo: foo.value }
               // skip for destructure patterns
-              if (!(parent as any).inPattern) {
+              if (
+                !(parent as any).inPattern ||
+                isInDestructureAssignment(parent, parentStack)
+              ) {
                 s.appendLeft(id.end! + startOffset, `: ${id.name}.value`)
               }
             } else {
@@ -1258,6 +1261,8 @@ function genRuntimeEmits(emits: Set<string>) {
     : ``
 }
 
+const parentStack: Node[] = []
+
 /**
  * Walk an AST and find identifiers that are variable references.
  * This is largely the same logic with `transformExpressions` in compiler-core
@@ -1270,10 +1275,11 @@ function walkIdentifiers(
 ) {
   const knownIds: Record<string, number> = Object.create(null)
   ;(walk as any)(root, {
-    enter(node: Node & { scopeIds?: Set<string> }, parent: Node) {
+    enter(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
+      parent && parentStack.push(parent)
       if (node.type === 'Identifier') {
-        if (!knownIds[node.name] && isRefIdentifier(node, parent)) {
-          onIdentifier(node, parent)
+        if (!knownIds[node.name] && isRefIdentifier(node, parent!)) {
+          onIdentifier(node, parent!)
         }
       } else if (isFunction(node)) {
         // walk function expressions and add its arguments to known identifiers
@@ -1309,13 +1315,14 @@ function walkIdentifiers(
         )
       } else if (
         node.type === 'ObjectProperty' &&
-        parent.type === 'ObjectPattern'
+        parent!.type === 'ObjectPattern'
       ) {
         // mark property in destructure pattern
         ;(node as any).inPattern = true
       }
     },
-    leave(node: Node & { scopeIds?: Set<string> }) {
+    leave(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
+      parent && parentStack.pop()
       if (node.scopeIds) {
         node.scopeIds.forEach((id: string) => {
           knownIds[id]--
@@ -1355,8 +1362,11 @@ function isRefIdentifier(id: Identifier, parent: Node) {
     return false
   }
 
-  // array destructure pattern
-  if (parent.type === 'ArrayPattern') {
+  // non-assignment array destructure pattern
+  if (
+    parent.type === 'ArrayPattern' &&
+    !isInDestructureAssignment(parent, parentStack)
+  ) {
     return false
   }
 
@@ -1464,6 +1474,27 @@ function canNeverBeRef(node: Node, userReactiveImport: string): boolean {
   }
 }
 
+function isInDestructureAssignment(parent: Node, parentStack: Node[]): boolean {
+  if (
+    parent &&
+    (parent.type === 'ObjectProperty' || parent.type === 'ArrayPattern')
+  ) {
+    let i = parentStack.length
+    while (i--) {
+      const p = parentStack[i]
+      if (p.type === 'AssignmentExpression') {
+        const root = parentStack[0]
+        // if this is a ref: destructure, it should be treated like a
+        // variable decalration!
+        return !(root.type === 'LabeledStatement' && root.label.name === 'ref')
+      } else if (p.type !== 'ObjectProperty' && !p.type.endsWith('Pattern')) {
+        break
+      }
+    }
+  }
+  return false
+}
+
 /**
  * Analyze bindings in normal `<script>`
  * Note that `compileScriptSetup` already analyzes bindings as part of its
index 7cf0914d46c94a19e580d66cf36379c2008e720f..d903a017a621e8a0e911c3c866c5c73333a8b3f7 100644 (file)
@@ -14,9 +14,11 @@ export const compilerOptions: CompilerOptions = reactive({
   inline: false,
   ssrCssVars: `{ color }`,
   bindingMetadata: {
-    TestComponent: BindingTypes.SETUP,
-    foo: BindingTypes.SETUP,
-    bar: BindingTypes.PROPS
+    TestComponent: BindingTypes.SETUP_CONST,
+    setupRef: BindingTypes.SETUP_REF,
+    setupLet: BindingTypes.SETUP_LET,
+    setupMaybeRef: BindingTypes.SETUP_MAYBE_REF,
+    setupProp: BindingTypes.PROPS
   }
 })