]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: properly handle assignment/update expressions in inline mode
authorEvan You <yyx990803@gmail.com>
Thu, 19 Nov 2020 00:38:18 +0000 (19:38 -0500)
committerEvan You <yyx990803@gmail.com>
Thu, 19 Nov 2020 00:38:38 +0000 (19:38 -0500)
packages/compiler-core/src/options.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-core/src/transforms/vModel.ts
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts

index bb59f4940facc9a79a7e8c8a8f25a9178b07fe55..43d019b753ecedf9b8efdbd98cbef2cb94b32791 100644 (file)
@@ -83,7 +83,11 @@ export const enum BindingTypes {
   /**
    * a const binding that may be a ref.
    */
-  SETUP_CONST_REF = 'setup-const-ref',
+  SETUP_MAYBE_REF = 'setup-maybe-ref',
+  /**
+   * bindings that are guaranteed to be refs
+   */
+  SETUP_REF = 'setup-ref',
   /**
    * declared by other options, e.g. computed, inject
    */
@@ -91,7 +95,7 @@ export const enum BindingTypes {
 }
 
 export interface BindingMetadata {
-  [key: string]: BindingTypes
+  [key: string]: BindingTypes | undefined
 }
 
 interface SharedTransformCodegenOptions {
index e3989a807eea66c44ba61858ab244fa2ea26dbf1..1d4a46a133ea56dbc2fceb3e1cee9be8243af87e 100644 (file)
@@ -272,7 +272,8 @@ export function resolveComponentType(
     }
     const tagFromSetup =
       checkType(BindingTypes.SETUP_LET) ||
-      checkType(BindingTypes.SETUP_CONST_REF)
+      checkType(BindingTypes.SETUP_REF) ||
+      checkType(BindingTypes.SETUP_MAYBE_REF)
     if (tagFromSetup) {
       return context.inline
         ? // setup scope bindings that may be refs need to be unrefed
index 9c33cf67b6aef67e63ba098533888d7e1177f629..edee2f52ec3c99d774a226529e6c98d5ede1ec34 100644 (file)
@@ -21,14 +21,22 @@ import {
   isGloballyWhitelisted,
   makeMap,
   babelParserDefaultPlugins,
-  hasOwn
+  hasOwn,
+  isString
 } from '@vue/shared'
 import { createCompilerError, ErrorCodes } from '../errors'
-import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
+import {
+  Node,
+  Function,
+  Identifier,
+  ObjectProperty,
+  AssignmentExpression,
+  UpdateExpression
+} from '@babel/types'
 import { validateBrowserExpression } from '../validateExpression'
 import { parse } from '@babel/parser'
 import { walk } from 'estree-walker'
-import { UNREF } from '../runtimeHelpers'
+import { IS_REF, UNREF } from '../runtimeHelpers'
 import { BindingTypes } from '../options'
 
 const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
@@ -100,28 +108,81 @@ export function processExpression(
   }
 
   const { inline, bindingMetadata } = context
-  const prefix = (raw: string) => {
+  const rewriteIdentifier = (raw: string, parent?: Node, id?: Identifier) => {
     const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
     if (inline) {
+      const isAssignmentLVal =
+        parent && parent.type === 'AssignmentExpression' && parent.left === id
+      const isUpdateArg =
+        parent && parent.type === 'UpdateExpression' && parent.argument === id
       // setup inline mode
       if (type === BindingTypes.SETUP_CONST) {
         return raw
+      } else if (type === BindingTypes.SETUP_REF) {
+        return isAssignmentLVal || isUpdateArg
+          ? `${raw}.value`
+          : `${context.helperString(UNREF)}(${raw})`
       } else if (
-        type === BindingTypes.SETUP_CONST_REF ||
+        type === BindingTypes.SETUP_MAYBE_REF ||
         type === BindingTypes.SETUP_LET
       ) {
-        return `${context.helperString(UNREF)}(${raw})`
+        if (isAssignmentLVal) {
+          if (type === BindingTypes.SETUP_MAYBE_REF) {
+            // const binding that may or may not be ref
+            // if it's not a ref, then the assignment doesn't make sense so
+            // just no-op it
+            // x = y ---> !isRef(x) ? null : x.value = y
+            return `!${context.helperString(
+              IS_REF
+            )}(${raw}) ? null : ${raw}.value`
+          } else {
+            // let binding.
+            // this is a bit more tricky as we need to cover the case where
+            // let is a local non-ref value, and we need to replicate the
+            // right hand side value.
+            // x = y --> isRef(x) ? x.value = y : x = y
+            const rVal = (parent as AssignmentExpression).right
+            const rExp = rawExp.slice(rVal.start! - 1, rVal.end! - 1)
+            const rExpString = stringifyExpression(
+              processExpression(createSimpleExpression(rExp, false), context)
+            )
+            return `${context.helperString(IS_REF)}(${raw})${
+              context.isTS ? ` //@ts-ignore\n` : ``
+            } ? ${raw}.value = ${rExpString} : ${raw}`
+          }
+        } else if (isUpdateArg) {
+          // make id replace parent in the code range so the raw update operator
+          // is removed
+          id!.start = parent!.start
+          id!.end = parent!.end
+          const { prefix: isPrefix, operator } = parent as UpdateExpression
+          const prefix = isPrefix ? operator : ``
+          const postfix = isPrefix ? `` : operator
+          if (type === BindingTypes.SETUP_MAYBE_REF) {
+            // const binding that may or may not be ref
+            // if it's not a ref, then the assignment doesn't make sense so
+            // just no-op it
+            // x++ ---> !isRef(x) ? null : x.value++
+            return `!${context.helperString(
+              IS_REF
+            )}(${raw}) ? null : ${prefix}${raw}.value${postfix}`
+          } else {
+            // let binding.
+            // x++ --> isRef(a) ? a.value++ : a++
+            return `${context.helperString(IS_REF)}(${raw})${
+              context.isTS ? ` //@ts-ignore\n` : ``
+            } ? ${prefix}${raw}.value${postfix} : ${prefix}${raw}${postfix}`
+          }
+        } else {
+          return `${context.helperString(UNREF)}(${raw})`
+        }
       } else if (type === BindingTypes.PROPS) {
         // use __props which is generated by compileScript so in ts mode
         // it gets correct type
         return `__props.${raw}`
       }
     } else {
-      if (
-        type === BindingTypes.SETUP_LET ||
-        type === BindingTypes.SETUP_CONST ||
-        type === BindingTypes.SETUP_CONST_REF
-      ) {
+      if (type && type.startsWith('setup')) {
         // setup bindings in non-inline mode
         return `$setup.${raw}`
       } else if (type) {
@@ -149,7 +210,7 @@ export function processExpression(
       !isGloballyWhitelisted(rawExp) &&
       !isLiteralWhitelisted(rawExp)
     ) {
-      node.content = prefix(rawExp)
+      node.content = rewriteIdentifier(rawExp)
     } else if (!context.identifiers[rawExp] && !bailConstant) {
       // mark node constant for hoisting unless it's referring a scope variable
       node.isConstant = true
@@ -199,7 +260,7 @@ export function processExpression(
               // we rewrite the value
               node.prefix = `${node.name}: `
             }
-            node.name = prefix(node.name)
+            node.name = rewriteIdentifier(node.name, parent, node)
             ids.push(node)
           } else if (!isStaticPropertyKey(node, parent)) {
             // The identifier is considered constant unless it's pointing to a
@@ -373,3 +434,15 @@ function shouldPrefix(id: Identifier, parent: Node) {
 
   return true
 }
+
+function stringifyExpression(exp: ExpressionNode | string): string {
+  if (isString(exp)) {
+    return exp
+  } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
+    return exp.content
+  } else {
+    return (exp.children as (ExpressionNode | string)[])
+      .map(stringifyExpression)
+      .join('')
+  }
+}
index 0274b642e5a90b95ac4a67ebfca6094fc81ab453..7bdffd81c0aa791cb4fa14d9547165ae17813434 100644 (file)
@@ -5,7 +5,8 @@ import {
   createCompoundExpression,
   NodeTypes,
   Property,
-  ElementTypes
+  ElementTypes,
+  ExpressionNode
 } from '../ast'
 import { createCompilerError, ErrorCodes } from '../errors'
 import {
@@ -14,7 +15,8 @@ import {
   hasScopeRef,
   isStaticExp
 } from '../utils'
-import { helperNameMap, IS_REF, UNREF } from '../runtimeHelpers'
+import { IS_REF } from '../runtimeHelpers'
+import { BindingTypes } from '../options'
 
 export const transformModel: DirectiveTransform = (dir, node, context) => {
   const { exp, arg } = dir
@@ -31,10 +33,14 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
 
   // im SFC <script setup> inline mode, the exp may have been transformed into
   // _unref(exp)
-  const isUnrefExp =
-    !__BROWSER__ && expString.startsWith(`_${helperNameMap[UNREF]}`)
+  const bindingType = context.bindingMetadata[rawExp]
+  const maybeRef =
+    !__BROWSER__ &&
+    context.inline &&
+    bindingType &&
+    bindingType !== BindingTypes.SETUP_CONST
 
-  if (!isMemberExpression(expString) && !isUnrefExp) {
+  if (!isMemberExpression(expString) && !maybeRef) {
     context.onError(
       createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc)
     )
@@ -60,25 +66,40 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
       : createCompoundExpression(['"onUpdate:" + ', arg])
     : `onUpdate:modelValue`
 
-  const assigmentExp = isUnrefExp
-    ? // v-model used on a potentially ref binding in <script setup> inline mode.
+  let assignmentExp: ExpressionNode
+  const eventArg = context.isTS ? `($event: any)` : `$event`
+  if (maybeRef) {
+    if (bindingType === BindingTypes.SETUP_REF) {
+      // v-model used on known ref.
+      assignmentExp = createCompoundExpression([
+        `${eventArg} => (`,
+        createSimpleExpression(rawExp, false, exp.loc),
+        `.value = $event)`
+      ])
+    } else {
+      // v-model used on a potentially ref binding in <script setup> inline mode.
       // the assignment needs to check whether the binding is actually a ref.
-      createSimpleExpression(
-        `$event => (${context.helperString(IS_REF)}(${rawExp}) ` +
-          `? (${rawExp}.value = $event) ` +
-          `: ${context.isTS ? `//@ts-ignore\n` : ``}` +
-          `(${rawExp} = $event)` +
-          `)`,
-        false,
-        exp.loc
-      )
-    : createCompoundExpression([`$event => (`, exp, ` = $event)`])
+      const altAssignment =
+        bindingType === BindingTypes.SETUP_LET ? `${rawExp} = $event` : `null`
+      assignmentExp = createCompoundExpression([
+        `${eventArg} => (${context.helperString(IS_REF)}(${rawExp}) ? `,
+        createSimpleExpression(rawExp, false, exp.loc),
+        `.value = $event : ${altAssignment})`
+      ])
+    }
+  } else {
+    assignmentExp = createCompoundExpression([
+      `${eventArg} => (`,
+      exp,
+      ` = $event)`
+    ])
+  }
 
   const props = [
     // modelValue: foo
     createObjectProperty(propName, dir.exp!),
     // "onUpdate:modelValue": $event => (foo = $event)
-    createObjectProperty(eventName, assigmentExp)
+    createObjectProperty(eventName, assignmentExp)
   ]
 
   // cache v-model handler if applicable (when it doesn't refer any scope vars)
index 1664ee4b173efd8646b8b896b32f472708e840b6..a08802a8c58eb0e3d545e6557065a56f6b38ea0b 100644 (file)
@@ -166,8 +166,8 @@ return (_ctx, _cache) => {
 }"
 `;
 
-exports[`SFC compile <script setup> inlineTemplate mode v-model codegen with unref() 1`] = `
-"import { unref as _unref, isRef as _isRef, vModelText as _vModelText, createVNode as _createVNode, withDirectives as _withDirectives, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
+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 { ref } from 'vue'
         
@@ -176,13 +176,98 @@ export default {
   setup(__props) {
 
         const count = ref(0)
+        const maybe = foo()
+        let lett = 1
         
 return (_ctx, _cache) => {
-  return _withDirectives((_openBlock(), _createBlock(\\"input\\", {
-    \\"onUpdate:modelValue\\": _cache[1] || (_cache[1] = $event => (_isRef(count) ? (count.value = $event) : (count = $event)))
-  }, null, 512 /* NEED_PATCH */)), [
-    [_vModelText, _unref(count)]
-  ])
+  return (_openBlock(), _createBlock(_Fragment, null, [
+    _createVNode(\\"div\\", {
+      onClick: _cache[1] || (_cache[1] = $event => (count.value = 1))
+    }),
+    _createVNode(\\"div\\", {
+      onClick: _cache[2] || (_cache[2] = $event => (!_isRef(maybe) ? null : maybe.value = _unref(count)))
+    }),
+    _createVNode(\\"div\\", {
+      onClick: _cache[3] || (_cache[3] = $event => (_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)))
+    })
+  ], 64 /* STABLE_FRAGMENT */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> inlineTemplate mode template update expression codegen 1`] = `
+"import { createVNode as _createVNode, isRef as _isRef, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
+
+import { ref } from 'vue'
+        
+export default {
+  expose: [],
+  setup(__props) {
+
+        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.value++))
+    }),
+    _createVNode(\\"div\\", {
+      onClick: _cache[2] || (_cache[2] = $event => (--count.value))
+    }),
+    _createVNode(\\"div\\", {
+      onClick: _cache[3] || (_cache[3] = $event => (!_isRef(maybe) ? null : maybe.value++))
+    }),
+    _createVNode(\\"div\\", {
+      onClick: _cache[4] || (_cache[4] = $event => (!_isRef(maybe) ? null : --maybe.value))
+    }),
+    _createVNode(\\"div\\", {
+      onClick: _cache[5] || (_cache[5] = $event => (_isRef(lett) ? lett.value++ : lett++))
+    }),
+    _createVNode(\\"div\\", {
+      onClick: _cache[6] || (_cache[6] = $event => (_isRef(lett) ? --lett.value : --lett))
+    })
+  ], 64 /* STABLE_FRAGMENT */))
+}
+}
+
+}"
+`;
+
+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 { ref } from 'vue'
+        
+export default {
+  expose: [],
+  setup(__props) {
+
+        const count = ref(0)
+        const maybe = foo()
+        let lett = 1
+        
+return (_ctx, _cache) => {
+  return (_openBlock(), _createBlock(_Fragment, null, [
+    _withDirectives(_createVNode(\\"input\\", {
+      \\"onUpdate:modelValue\\": _cache[1] || (_cache[1] = $event => (count.value = $event))
+    }, null, 512 /* NEED_PATCH */), [
+      [_vModelText, _unref(count)]
+    ]),
+    _withDirectives(_createVNode(\\"input\\", {
+      \\"onUpdate:modelValue\\": _cache[2] || (_cache[2] = $event => (_isRef(maybe) ? maybe.value = $event : null))
+    }, null, 512 /* NEED_PATCH */), [
+      [_vModelText, _unref(maybe)]
+    ]),
+    _withDirectives(_createVNode(\\"input\\", {
+      \\"onUpdate:modelValue\\": _cache[3] || (_cache[3] = $event => (_isRef(lett) ? lett.value = $event : lett = $event))
+    }, null, 512 /* NEED_PATCH */), [
+      [_vModelText, _unref(lett)]
+    ])
+  ], 64 /* STABLE_FRAGMENT */))
 }
 }
 
index 180b24732919fb25b1183c025bfc966488dc9945..a610d3be5fb1193282d3c8bb7d12b4726c72179b 100644 (file)
@@ -152,18 +152,90 @@ const bar = 1
       expect(content).not.toMatch(`PROPS`)
     })
 
-    test('v-model codegen with unref()', () => {
+    test('v-model codegen', () => {
       const { content } = compile(
         `<script setup>
         import { ref } from 'vue'
         const count = ref(0)
+        const maybe = foo()
+        let lett = 1
         </script>
         <template>
           <input v-model="count">
+          <input v-model="maybe">
+          <input v-model="lett">
         </template>
         `,
         { inlineTemplate: true }
       )
+      // 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`)
+      // let: handle both cases
+      expect(content).toMatch(
+        `_isRef(lett) ? lett.value = $event : lett = $event`
+      )
+      assertCode(content)
+    })
+
+    test('template assignment expression codegen', () => {
+      const { content } = compile(
+        `<script setup>
+        import { ref } from 'vue'
+        const count = ref(0)
+        const maybe = foo()
+        let lett = 1
+        </script>
+        <template>
+          <div @click="count = 1"/>
+          <div @click="maybe = count"/>
+          <div @click="lett = count"/>
+        </template>
+        `,
+        { inlineTemplate: true }
+      )
+      // 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)`
+      )
+      // let: handle both cases
+      expect(content).toMatch(
+        `_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)`
+      )
+      assertCode(content)
+    })
+
+    test('template update expression codegen', () => {
+      const { content } = compile(
+        `<script setup>
+        import { ref } from 'vue'
+        const count = ref(0)
+        const maybe = foo()
+        let lett = 1
+        </script>
+        <template>
+          <div @click="count++"/>
+          <div @click="--count"/>
+          <div @click="maybe++"/>
+          <div @click="--maybe"/>
+          <div @click="lett++"/>
+          <div @click="--lett"/>
+        </template>
+        `,
+        { inlineTemplate: true }
+      )
+      // 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`)
+      // let: handle both cases
+      expect(content).toMatch(`_isRef(lett) ? lett.value++ : lett++`)
+      expect(content).toMatch(`_isRef(lett) ? --lett.value : --lett`)
       assertCode(content)
     })
   })
@@ -381,9 +453,9 @@ const { props, emit } = defineOptions({
       expect(content).toMatch(`let d`)
       assertCode(content)
       expect(bindings).toStrictEqual({
-        foo: BindingTypes.SETUP_CONST_REF,
-        a: BindingTypes.SETUP_CONST_REF,
-        b: BindingTypes.SETUP_CONST_REF,
+        foo: BindingTypes.SETUP_REF,
+        a: BindingTypes.SETUP_REF,
+        b: BindingTypes.SETUP_REF,
         c: BindingTypes.SETUP_LET,
         d: BindingTypes.SETUP_LET
       })
@@ -403,9 +475,9 @@ const { props, emit } = defineOptions({
       expect(content).toMatch(`return { a, b, c }`)
       assertCode(content)
       expect(bindings).toStrictEqual({
-        a: BindingTypes.SETUP_CONST_REF,
-        b: BindingTypes.SETUP_CONST_REF,
-        c: BindingTypes.SETUP_CONST_REF
+        a: BindingTypes.SETUP_REF,
+        b: BindingTypes.SETUP_REF,
+        c: BindingTypes.SETUP_REF
       })
     })
 
@@ -495,12 +567,12 @@ const { props, emit } = defineOptions({
       )
       expect(content).toMatch(`return { n, a, c, d, f, g }`)
       expect(bindings).toStrictEqual({
-        n: BindingTypes.SETUP_CONST_REF,
-        a: BindingTypes.SETUP_CONST_REF,
-        c: BindingTypes.SETUP_CONST_REF,
-        d: BindingTypes.SETUP_CONST_REF,
-        f: BindingTypes.SETUP_CONST_REF,
-        g: BindingTypes.SETUP_CONST_REF
+        n: BindingTypes.SETUP_REF,
+        a: BindingTypes.SETUP_REF,
+        c: BindingTypes.SETUP_REF,
+        d: BindingTypes.SETUP_REF,
+        f: BindingTypes.SETUP_REF,
+        g: BindingTypes.SETUP_REF
       })
       assertCode(content)
     })
@@ -519,10 +591,10 @@ const { props, emit } = defineOptions({
       expect(content).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
       expect(content).toMatch(`return { n, a, b, c }`)
       expect(bindings).toStrictEqual({
-        n: BindingTypes.SETUP_CONST_REF,
-        a: BindingTypes.SETUP_CONST_REF,
-        b: BindingTypes.SETUP_CONST_REF,
-        c: BindingTypes.SETUP_CONST_REF
+        n: BindingTypes.SETUP_REF,
+        a: BindingTypes.SETUP_REF,
+        b: BindingTypes.SETUP_REF,
+        c: BindingTypes.SETUP_REF
       })
       assertCode(content)
     })
@@ -542,9 +614,9 @@ const { props, emit } = defineOptions({
       expect(content).toMatch(`\nconst e = _ref(__e);`)
       expect(content).toMatch(`return { b, d, e }`)
       expect(bindings).toStrictEqual({
-        b: BindingTypes.SETUP_CONST_REF,
-        d: BindingTypes.SETUP_CONST_REF,
-        e: BindingTypes.SETUP_CONST_REF
+        b: BindingTypes.SETUP_REF,
+        d: BindingTypes.SETUP_REF,
+        e: BindingTypes.SETUP_REF
       })
       assertCode(content)
     })
@@ -728,8 +800,8 @@ describe('SFC analyze <script> bindings', () => {
       </script>
     `)
     expect(bindings).toStrictEqual({
-      foo: BindingTypes.SETUP_CONST_REF,
-      bar: BindingTypes.SETUP_CONST_REF
+      foo: BindingTypes.SETUP_MAYBE_REF,
+      bar: BindingTypes.SETUP_MAYBE_REF
     })
   })
 
@@ -748,8 +820,8 @@ describe('SFC analyze <script> bindings', () => {
       </script>
     `)
     expect(bindings).toStrictEqual({
-      foo: BindingTypes.SETUP_CONST_REF,
-      bar: BindingTypes.SETUP_CONST_REF
+      foo: BindingTypes.SETUP_MAYBE_REF,
+      bar: BindingTypes.SETUP_MAYBE_REF
     })
   })
 
@@ -867,7 +939,7 @@ describe('SFC analyze <script> bindings', () => {
     expect(bindings).toStrictEqual({
       foo: BindingTypes.OPTIONS,
       bar: BindingTypes.PROPS,
-      baz: BindingTypes.SETUP_CONST_REF,
+      baz: BindingTypes.SETUP_MAYBE_REF,
       qux: BindingTypes.DATA,
       quux: BindingTypes.OPTIONS,
       quuz: BindingTypes.OPTIONS
@@ -877,15 +949,28 @@ describe('SFC analyze <script> bindings', () => {
   it('works for script setup', () => {
     const { bindings } = compile(`
       <script setup>
-      import { defineOptions } from 'vue'
+      import { defineOptions, ref as r } from 'vue'
       defineOptions({
         props: {
           foo: String,
         }
       })
+
+      const a = r(1)
+      let b = 2
+      const c = 3
+      const { d } = someFoo()
+      let { e } = someBar()
       </script>
     `)
+
     expect(bindings).toStrictEqual({
+      r: BindingTypes.SETUP_CONST,
+      a: BindingTypes.SETUP_REF,
+      b: BindingTypes.SETUP_LET,
+      c: BindingTypes.SETUP_CONST,
+      d: BindingTypes.SETUP_MAYBE_REF,
+      e: BindingTypes.SETUP_LET,
       foo: BindingTypes.PROPS
     })
   })
index bdbd0b1d3e17603c303af1ab7738089638809c93..4e1d1288df2ac026b44afa510cf2a16919560114 100644 (file)
@@ -20,7 +20,8 @@ import {
   Statement,
   Expression,
   LabeledStatement,
-  TSUnionType
+  TSUnionType,
+  CallExpression
 } from '@babel/types'
 import { walk } from 'estree-walker'
 import { RawSourceMap } from 'source-map'
@@ -159,6 +160,7 @@ export function compileScript(
       source: string
     }
   > = Object.create(null)
+  const userImportAlias: Record<string, string> = Object.create(null)
   const setupBindings: Record<string, BindingTypes> = Object.create(null)
   const refBindings: Record<string, BindingTypes> = Object.create(null)
   const refIdentifiers: Set<Identifier> = new Set()
@@ -220,12 +222,22 @@ export function compileScript(
     )
   }
 
+  function registerUserImport(
+    source: string,
+    local: string,
+    imported: string | false
+  ) {
+    if (source === 'vue' && imported) {
+      userImportAlias[imported] = local
+    }
+    userImports[local] = {
+      imported: imported || null,
+      source
+    }
+  }
+
   function processDefineOptions(node: Node): boolean {
-    if (
-      node.type === 'CallExpression' &&
-      node.callee.type === 'Identifier' &&
-      node.callee.name === DEFINE_OPTIONS
-    ) {
+    if (isCallOf(node, DEFINE_OPTIONS)) {
       if (hasOptionsCall) {
         error(`duplicate ${DEFINE_OPTIONS}() call`, node)
       }
@@ -308,7 +320,7 @@ export function compileScript(
     if (id.name[0] === '$') {
       error(`ref variable identifiers cannot start with $.`, id)
     }
-    refBindings[id.name] = setupBindings[id.name] = BindingTypes.SETUP_CONST_REF
+    refBindings[id.name] = setupBindings[id.name] = BindingTypes.SETUP_REF
     refIdentifiers.add(id)
   }
 
@@ -409,15 +421,11 @@ export function compileScript(
       if (node.type === 'ImportDeclaration') {
         // record imports for dedupe
         for (const specifier of node.specifiers) {
-          const name = specifier.local.name
           const imported =
             specifier.type === 'ImportSpecifier' &&
             specifier.imported.type === 'Identifier' &&
             specifier.imported.name
-          userImports[name] = {
-            imported: imported || null,
-            source: node.source.value
-          }
+          registerUserImport(node.source.value, specifier.local.name, imported)
         }
       } else if (node.type === 'ExportDefaultDeclaration') {
         // export default
@@ -567,10 +575,7 @@ export function compileScript(
             error(`different imports aliased to same local name.`, specifier)
           }
         } else {
-          userImports[local] = {
-            imported: imported || null,
-            source: node.source.value
-          }
+          registerUserImport(source, local, imported)
         }
       }
       if (removed === node.specifiers.length) {
@@ -605,7 +610,7 @@ export function compileScript(
         node.type === 'ClassDeclaration') &&
       !node.declare
     ) {
-      walkDeclaration(node, setupBindings)
+      walkDeclaration(node, setupBindings, userImportAlias)
     }
 
     // Type declarations
@@ -783,9 +788,10 @@ export function compileScript(
     Object.assign(bindingMetadata, analyzeBindingsFromOptions(optionsArg))
   }
   for (const [key, { source }] of Object.entries(userImports)) {
-    bindingMetadata[key] = source.endsWith('.vue')
-      ? BindingTypes.SETUP_CONST
-      : BindingTypes.SETUP_CONST_REF
+    bindingMetadata[key] =
+      source.endsWith('.vue') || source === 'vue'
+        ? BindingTypes.SETUP_CONST
+        : BindingTypes.SETUP_MAYBE_REF
   }
   for (const key in setupBindings) {
     bindingMetadata[key] = setupBindings[key]
@@ -941,32 +947,34 @@ export function compileScript(
 
 function walkDeclaration(
   node: Declaration,
-  bindings: Record<string, BindingTypes>
+  bindings: Record<string, BindingTypes>,
+  userImportAlias: Record<string, string>
 ) {
   if (node.type === 'VariableDeclaration') {
     const isConst = node.kind === 'const'
     // export const foo = ...
     for (const { id, init } of node.declarations) {
-      const isUseOptionsCall = !!(
-        isConst &&
-        init &&
-        init.type === 'CallExpression' &&
-        init.callee.type === 'Identifier' &&
-        init.callee.name === DEFINE_OPTIONS
-      )
+      const isUseOptionsCall = !!(isConst && isCallOf(init, DEFINE_OPTIONS))
       if (id.type === 'Identifier') {
-        bindings[id.name] =
+        let bindingType
+        if (
           // if a declaration is a const literal, we can mark it so that
           // the generated render fn code doesn't need to unref() it
           isUseOptionsCall ||
           (isConst &&
-          init!.type !== 'Identifier' && // const a = b
-          init!.type !== 'CallExpression' && // const a = ref()
-            init!.type !== 'MemberExpression') // const a = b.c
-            ? BindingTypes.SETUP_CONST
-            : isConst
-              ? BindingTypes.SETUP_CONST_REF
-              : BindingTypes.SETUP_LET
+            canNeverBeRef(init!, userImportAlias['reactive'] || 'reactive'))
+        ) {
+          bindingType = BindingTypes.SETUP_CONST
+        } else if (isConst) {
+          if (isCallOf(init, userImportAlias['ref'] || 'ref')) {
+            bindingType = BindingTypes.SETUP_REF
+          } else {
+            bindingType = BindingTypes.SETUP_MAYBE_REF
+          }
+        } else {
+          bindingType = BindingTypes.SETUP_LET
+        }
+        bindings[id.name] = bindingType
       } else if (id.type === 'ObjectPattern') {
         walkObjectPattern(id, bindings, isConst, isUseOptionsCall)
       } else if (id.type === 'ArrayPattern') {
@@ -998,7 +1006,7 @@ function walkObjectPattern(
           bindings[p.key.name] = isUseOptionsCall
             ? BindingTypes.SETUP_CONST
             : isConst
-              ? BindingTypes.SETUP_CONST_REF
+              ? BindingTypes.SETUP_MAYBE_REF
               : BindingTypes.SETUP_LET
         } else {
           walkPattern(p.value, bindings, isConst, isUseOptionsCall)
@@ -1035,7 +1043,7 @@ function walkPattern(
     bindings[node.name] = isUseOptionsCall
       ? BindingTypes.SETUP_CONST
       : isConst
-        ? BindingTypes.SETUP_CONST_REF
+        ? BindingTypes.SETUP_MAYBE_REF
         : BindingTypes.SETUP_LET
   } else if (node.type === 'RestElement') {
     // argument can only be identifer when destructuring
@@ -1051,7 +1059,7 @@ function walkPattern(
       bindings[node.left.name] = isUseOptionsCall
         ? BindingTypes.SETUP_CONST
         : isConst
-          ? BindingTypes.SETUP_CONST_REF
+          ? BindingTypes.SETUP_MAYBE_REF
           : BindingTypes.SETUP_LET
     } else {
       walkPattern(node.left, bindings, isConst)
@@ -1419,6 +1427,43 @@ function getObjectOrArrayExpressionKeys(property: ObjectProperty): string[] {
   return []
 }
 
+function isCallOf(node: Node | null, name: string): node is CallExpression {
+  return !!(
+    node &&
+    node.type === 'CallExpression' &&
+    node.callee.type === 'Identifier' &&
+    node.callee.name === name
+  )
+}
+
+function canNeverBeRef(node: Node, userReactiveImport: string): boolean {
+  if (isCallOf(node, userReactiveImport)) {
+    return true
+  }
+  switch (node.type) {
+    case 'UnaryExpression':
+    case 'BinaryExpression':
+    case 'ArrayExpression':
+    case 'ObjectExpression':
+    case 'FunctionExpression':
+    case 'ArrowFunctionExpression':
+    case 'UpdateExpression':
+    case 'ClassExpression':
+    case 'TaggedTemplateExpression':
+      return true
+    case 'SequenceExpression':
+      return canNeverBeRef(
+        node.expressions[node.expressions.length - 1],
+        userReactiveImport
+      )
+    default:
+      if (node.type.endsWith('Literal')) {
+        return true
+      }
+      return false
+  }
+}
+
 /**
  * Analyze bindings in normal `<script>`
  * Note that `compileScriptSetup` already analyzes bindings as part of its
@@ -1495,7 +1540,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
           for (const key of getObjectExpressionKeys(bodyItem.argument)) {
             bindings[key] =
               property.key.name === 'setup'
-                ? BindingTypes.SETUP_CONST_REF
+                ? BindingTypes.SETUP_MAYBE_REF
                 : BindingTypes.DATA
           }
         }