]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(reactivity-transform): prohibit const assignment at compile time (#6993)
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Mon, 14 Nov 2022 07:54:41 +0000 (15:54 +0800)
committerGitHub <noreply@github.com>
Mon, 14 Nov 2022 07:54:41 +0000 (02:54 -0500)
close #6992

packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/reactivity-transform/__tests__/reactivityTransform.spec.ts
packages/reactivity-transform/src/reactivityTransform.ts

index 7d35f91abf273e1e0975d347dbaad9a8a30586c2..6fcc0e59068f37f5a4ffd91c85a46580d0a448a7 100644 (file)
@@ -247,5 +247,16 @@ describe('sfc props transform', () => {
         )
       ).toThrow(`cannot reference locally declared variables`)
     })
+
+    test('should error if assignment to constant variable', () => {
+      expect(() =>
+        compile(
+          `<script setup>
+          const { foo } = defineProps(['foo'])
+          foo = 'bar'
+          </script>`
+        )
+      ).toThrow(`Assignment to constant variable.`)
+    })
   })
 })
index bb4e7f42c135479fecd5c7f3b22de0d2c1128607..997d7c11b9fe6a97188d7b82d836f4ef2f8d9ca8 100644 (file)
@@ -41,7 +41,8 @@ import {
   Program,
   ObjectMethod,
   LVal,
-  Expression
+  Expression,
+  VariableDeclaration
 } from '@babel/types'
 import { walk } from 'estree-walker'
 import { RawSourceMap } from 'source-map'
@@ -310,6 +311,7 @@ export function compileScript(
     {
       local: string // local identifier, may be different
       default?: Expression
+      isConst: boolean
     }
   > = Object.create(null)
 
@@ -404,7 +406,11 @@ export function compileScript(
     }
   }
 
-  function processDefineProps(node: Node, declId?: LVal): boolean {
+  function processDefineProps(
+    node: Node,
+    declId?: LVal,
+    declKind?: VariableDeclaration['kind']
+  ): boolean {
     if (!isCallOf(node, DEFINE_PROPS)) {
       return false
     }
@@ -442,6 +448,7 @@ export function compileScript(
     }
 
     if (declId) {
+      const isConst = declKind === 'const'
       if (enablePropsTransform && declId.type === 'ObjectPattern') {
         propsDestructureDecl = declId
         // props destructure - handle compilation sugar
@@ -468,12 +475,14 @@ export function compileScript(
               // store default value
               propsDestructuredBindings[propKey] = {
                 local: left.name,
-                default: right
+                default: right,
+                isConst
               }
             } else if (prop.value.type === 'Identifier') {
               // simple destructure
               propsDestructuredBindings[propKey] = {
-                local: prop.value.name
+                local: prop.value.name,
+                isConst
               }
             } else {
               error(
@@ -494,11 +503,15 @@ export function compileScript(
     return true
   }
 
-  function processWithDefaults(node: Node, declId?: LVal): boolean {
+  function processWithDefaults(
+    node: Node,
+    declId?: LVal,
+    declKind?: VariableDeclaration['kind']
+  ): boolean {
     if (!isCallOf(node, WITH_DEFAULTS)) {
       return false
     }
-    if (processDefineProps(node.arguments[0], declId)) {
+    if (processDefineProps(node.arguments[0], declId, declKind)) {
       if (propsRuntimeDecl) {
         error(
           `${WITH_DEFAULTS} can only be used with type-based ` +
@@ -1197,8 +1210,8 @@ export function compileScript(
         if (decl.init) {
           // defineProps / defineEmits
           const isDefineProps =
-            processDefineProps(decl.init, decl.id) ||
-            processWithDefaults(decl.init, decl.id)
+            processDefineProps(decl.init, decl.id, node.kind) ||
+            processWithDefaults(decl.init, decl.id, node.kind)
           const isDefineEmits = processDefineEmits(decl.init, decl.id)
           if (isDefineProps || isDefineEmits) {
             if (left === 1) {
index d1e0368f6865ac128dd212e41698195ef0fad2dc..5255be6ba8b4c4267de7698549fea668d57b70fe 100644 (file)
@@ -531,4 +531,34 @@ describe('errors', () => {
       `does not support rest element`
     )
   })
+
+  test('assignment to constant variable', () => {
+    expect(() =>
+      transform(`
+        const foo = $ref(0)
+        foo = 1
+      `)
+    ).toThrow(`Assignment to constant variable.`)
+
+    expect(() =>
+      transform(`
+        const [a, b] = $([1, 2])
+        a = 1
+      `)
+    ).toThrow(`Assignment to constant variable.`)
+
+    expect(() =>
+      transform(`
+        const foo = $ref(0)
+        foo++
+      `)
+    ).toThrow(`Assignment to constant variable.`)
+
+    expect(() =>
+      transform(`
+      const foo = $ref(0)
+      bar = foo
+      `)
+    ).not.toThrow()
+  })
 })
index 4f6d47a6d844b0f55caa89f0b8e39f0d95668900..f1d5d8916fda5f811272ba103a9713b8c1fb2924 100644 (file)
@@ -37,7 +37,11 @@ export function shouldTransform(src: string): boolean {
   return transformCheckRE.test(src)
 }
 
-type Scope = Record<string, boolean | 'prop'>
+interface Binding {
+  isConst?: boolean
+  isProp?: boolean
+}
+type Scope = Record<string, Binding | false>
 
 export interface RefTransformOptions {
   filename?: string
@@ -118,6 +122,7 @@ export function transformAST(
     {
       local: string // local identifier, may be different
       default?: any
+      isConst?: boolean
     }
   >
 ): {
@@ -168,17 +173,20 @@ export function transformAST(
   let escapeScope: CallExpression | undefined // inside $$()
   const excludedIds = new WeakSet<Identifier>()
   const parentStack: Node[] = []
-  const propsLocalToPublicMap = Object.create(null)
+  const propsLocalToPublicMap: Record<string, string> = Object.create(null)
 
   if (knownRefs) {
     for (const key of knownRefs) {
-      rootScope[key] = true
+      rootScope[key] = {}
     }
   }
   if (knownProps) {
     for (const key in knownProps) {
-      const { local } = knownProps[key]
-      rootScope[local] = 'prop'
+      const { local, isConst } = knownProps[key]
+      rootScope[local] = {
+        isProp: true,
+        isConst: !!isConst
+      }
       propsLocalToPublicMap[local] = key
     }
   }
@@ -218,7 +226,7 @@ export function transformAST(
     return false
   }
 
-  function error(msg: string, node: Node) {
+  function error(msg: string, node: Node): never {
     const e = new Error(msg)
     ;(e as any).node = node
     throw e
@@ -229,10 +237,10 @@ export function transformAST(
     return `_${msg}`
   }
 
-  function registerBinding(id: Identifier, isRef = false) {
+  function registerBinding(id: Identifier, binding?: Binding) {
     excludedIds.add(id)
     if (currentScope) {
-      currentScope[id.name] = isRef
+      currentScope[id.name] = binding ? binding : false
     } else {
       error(
         'registerBinding called without active scope, something is wrong.',
@@ -241,7 +249,8 @@ export function transformAST(
     }
   }
 
-  const registerRefBinding = (id: Identifier) => registerBinding(id, true)
+  const registerRefBinding = (id: Identifier, isConst = false) =>
+    registerBinding(id, { isConst })
 
   let tempVarCount = 0
   function genTempVar() {
@@ -296,7 +305,12 @@ export function transformAST(
         isCall &&
         (refCall = isRefCreationCall((decl as any).init.callee.name))
       ) {
-        processRefDeclaration(refCall, decl.id, decl.init as CallExpression)
+        processRefDeclaration(
+          refCall,
+          decl.id,
+          decl.init as CallExpression,
+          stmt.kind === 'const'
+        )
       } else {
         const isProps =
           isRoot && isCall && (decl as any).init.callee.name === 'defineProps'
@@ -316,7 +330,8 @@ export function transformAST(
   function processRefDeclaration(
     method: string,
     id: VariableDeclarator['id'],
-    call: CallExpression
+    call: CallExpression,
+    isConst: boolean
   ) {
     excludedIds.add(call.callee as Identifier)
     if (method === convertSymbol) {
@@ -325,16 +340,16 @@ export function transformAST(
       s.remove(call.callee.start! + offset, call.callee.end! + offset)
       if (id.type === 'Identifier') {
         // single variable
-        registerRefBinding(id)
+        registerRefBinding(id, isConst)
       } else if (id.type === 'ObjectPattern') {
-        processRefObjectPattern(id, call)
+        processRefObjectPattern(id, call, isConst)
       } else if (id.type === 'ArrayPattern') {
-        processRefArrayPattern(id, call)
+        processRefArrayPattern(id, call, isConst)
       }
     } else {
       // shorthands
       if (id.type === 'Identifier') {
-        registerRefBinding(id)
+        registerRefBinding(id, isConst)
         // replace call
         s.overwrite(
           call.start! + offset,
@@ -350,6 +365,7 @@ export function transformAST(
   function processRefObjectPattern(
     pattern: ObjectPattern,
     call: CallExpression,
+    isConst: boolean,
     tempVar?: string,
     path: PathSegment[] = []
   ) {
@@ -384,21 +400,27 @@ export function transformAST(
             // { foo: bar }
             nameId = p.value
           } else if (p.value.type === 'ObjectPattern') {
-            processRefObjectPattern(p.value, call, tempVar, [...path, key])
+            processRefObjectPattern(p.value, call, isConst, tempVar, [
+              ...path,
+              key
+            ])
           } else if (p.value.type === 'ArrayPattern') {
-            processRefArrayPattern(p.value, call, tempVar, [...path, key])
+            processRefArrayPattern(p.value, call, isConst, tempVar, [
+              ...path,
+              key
+            ])
           } else if (p.value.type === 'AssignmentPattern') {
             if (p.value.left.type === 'Identifier') {
               // { foo: bar = 1 }
               nameId = p.value.left
               defaultValue = p.value.right
             } else if (p.value.left.type === 'ObjectPattern') {
-              processRefObjectPattern(p.value.left, call, tempVar, [
+              processRefObjectPattern(p.value.left, call, isConst, tempVar, [
                 ...path,
                 [key, p.value.right]
               ])
             } else if (p.value.left.type === 'ArrayPattern') {
-              processRefArrayPattern(p.value.left, call, tempVar, [
+              processRefArrayPattern(p.value.left, call, isConst, tempVar, [
                 ...path,
                 [key, p.value.right]
               ])
@@ -412,7 +434,7 @@ export function transformAST(
         error(`reactivity destructure does not support rest elements.`, p)
       }
       if (nameId) {
-        registerRefBinding(nameId)
+        registerRefBinding(nameId, isConst)
         // inject toRef() after original replaced pattern
         const source = pathToString(tempVar, path)
         const keyStr = isString(key)
@@ -437,6 +459,7 @@ export function transformAST(
   function processRefArrayPattern(
     pattern: ArrayPattern,
     call: CallExpression,
+    isConst: boolean,
     tempVar?: string,
     path: PathSegment[] = []
   ) {
@@ -462,12 +485,12 @@ export function transformAST(
         // [...a]
         error(`reactivity destructure does not support rest elements.`, e)
       } else if (e.type === 'ObjectPattern') {
-        processRefObjectPattern(e, call, tempVar, [...path, i])
+        processRefObjectPattern(e, call, isConst, tempVar, [...path, i])
       } else if (e.type === 'ArrayPattern') {
-        processRefArrayPattern(e, call, tempVar, [...path, i])
+        processRefArrayPattern(e, call, isConst, tempVar, [...path, i])
       }
       if (nameId) {
-        registerRefBinding(nameId)
+        registerRefBinding(nameId, isConst)
         // inject toRef() after original replaced pattern
         const source = pathToString(tempVar, path)
         const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
@@ -520,9 +543,18 @@ export function transformAST(
     parentStack: Node[]
   ): boolean {
     if (hasOwn(scope, id.name)) {
-      const bindingType = scope[id.name]
-      if (bindingType) {
-        const isProp = bindingType === 'prop'
+      const binding = scope[id.name]
+
+      if (binding) {
+        if (
+          binding.isConst &&
+          ((parent.type === 'AssignmentExpression' && id === parent.left) ||
+            parent.type === 'UpdateExpression')
+        ) {
+          error(`Assignment to constant variable.`, id)
+        }
+
+        const { isProp } = binding
         if (isStaticProperty(parent) && parent.shorthand) {
           // let binding used in a property shorthand
           // skip for destructure patterns
@@ -638,18 +670,20 @@ export function transformAST(
         return this.skip()
       }
 
-      if (
-        node.type === 'Identifier' &&
-        // if inside $$(), skip unless this is a destructured prop binding
-        !(escapeScope && rootScope[node.name] !== 'prop') &&
-        isReferencedIdentifier(node, parent!, parentStack) &&
-        !excludedIds.has(node)
-      ) {
-        // walk up the scope chain to check if id should be appended .value
-        let i = scopeStack.length
-        while (i--) {
-          if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
-            return
+      if (node.type === 'Identifier') {
+        const binding = rootScope[node.name]
+        if (
+          // if inside $$(), skip unless this is a destructured prop binding
+          !(escapeScope && (!binding || !binding.isProp)) &&
+          isReferencedIdentifier(node, parent!, parentStack) &&
+          !excludedIds.has(node)
+        ) {
+          // walk up the scope chain to check if id should be appended .value
+          let i = scopeStack.length
+          while (i--) {
+            if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
+              return
+            }
           }
         }
       }
@@ -729,7 +763,10 @@ export function transformAST(
   })
 
   return {
-    rootRefs: Object.keys(rootScope).filter(key => rootScope[key] === true),
+    rootRefs: Object.keys(rootScope).filter(key => {
+      const binding = rootScope[key]
+      return binding && !binding.isProp
+    }),
     importedHelpers: [...importedHelpers]
   }
 }