Skip to content

Commit 3427052

Browse files
authoredNov 14, 2022
fix(reactivity-transform): prohibit const assignment at compile time (#6993)
close #6992
1 parent 87c72ae commit 3427052

File tree

4 files changed

+138
-47
lines changed

4 files changed

+138
-47
lines changed
 

‎packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts

+11
Original file line numberDiff line numberDiff line change
@@ -247,5 +247,16 @@ describe('sfc props transform', () => {
247247
)
248248
).toThrow(`cannot reference locally declared variables`)
249249
})
250+
251+
test('should error if assignment to constant variable', () => {
252+
expect(() =>
253+
compile(
254+
`<script setup>
255+
const { foo } = defineProps(['foo'])
256+
foo = 'bar'
257+
</script>`
258+
)
259+
).toThrow(`Assignment to constant variable.`)
260+
})
250261
})
251262
})

‎packages/compiler-sfc/src/compileScript.ts

+21-8
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ import {
4141
Program,
4242
ObjectMethod,
4343
LVal,
44-
Expression
44+
Expression,
45+
VariableDeclaration
4546
} from '@babel/types'
4647
import { walk } from 'estree-walker'
4748
import { RawSourceMap } from 'source-map'
@@ -310,6 +311,7 @@ export function compileScript(
310311
{
311312
local: string // local identifier, may be different
312313
default?: Expression
314+
isConst: boolean
313315
}
314316
> = Object.create(null)
315317

@@ -404,7 +406,11 @@ export function compileScript(
404406
}
405407
}
406408

407-
function processDefineProps(node: Node, declId?: LVal): boolean {
409+
function processDefineProps(
410+
node: Node,
411+
declId?: LVal,
412+
declKind?: VariableDeclaration['kind']
413+
): boolean {
408414
if (!isCallOf(node, DEFINE_PROPS)) {
409415
return false
410416
}
@@ -442,6 +448,7 @@ export function compileScript(
442448
}
443449

444450
if (declId) {
451+
const isConst = declKind === 'const'
445452
if (enablePropsTransform && declId.type === 'ObjectPattern') {
446453
propsDestructureDecl = declId
447454
// props destructure - handle compilation sugar
@@ -468,12 +475,14 @@ export function compileScript(
468475
// store default value
469476
propsDestructuredBindings[propKey] = {
470477
local: left.name,
471-
default: right
478+
default: right,
479+
isConst
472480
}
473481
} else if (prop.value.type === 'Identifier') {
474482
// simple destructure
475483
propsDestructuredBindings[propKey] = {
476-
local: prop.value.name
484+
local: prop.value.name,
485+
isConst
477486
}
478487
} else {
479488
error(
@@ -494,11 +503,15 @@ export function compileScript(
494503
return true
495504
}
496505

497-
function processWithDefaults(node: Node, declId?: LVal): boolean {
506+
function processWithDefaults(
507+
node: Node,
508+
declId?: LVal,
509+
declKind?: VariableDeclaration['kind']
510+
): boolean {
498511
if (!isCallOf(node, WITH_DEFAULTS)) {
499512
return false
500513
}
501-
if (processDefineProps(node.arguments[0], declId)) {
514+
if (processDefineProps(node.arguments[0], declId, declKind)) {
502515
if (propsRuntimeDecl) {
503516
error(
504517
`${WITH_DEFAULTS} can only be used with type-based ` +
@@ -1197,8 +1210,8 @@ export function compileScript(
11971210
if (decl.init) {
11981211
// defineProps / defineEmits
11991212
const isDefineProps =
1200-
processDefineProps(decl.init, decl.id) ||
1201-
processWithDefaults(decl.init, decl.id)
1213+
processDefineProps(decl.init, decl.id, node.kind) ||
1214+
processWithDefaults(decl.init, decl.id, node.kind)
12021215
const isDefineEmits = processDefineEmits(decl.init, decl.id)
12031216
if (isDefineProps || isDefineEmits) {
12041217
if (left === 1) {

‎packages/reactivity-transform/__tests__/reactivityTransform.spec.ts

+30
Original file line numberDiff line numberDiff line change
@@ -531,4 +531,34 @@ describe('errors', () => {
531531
`does not support rest element`
532532
)
533533
})
534+
535+
test('assignment to constant variable', () => {
536+
expect(() =>
537+
transform(`
538+
const foo = $ref(0)
539+
foo = 1
540+
`)
541+
).toThrow(`Assignment to constant variable.`)
542+
543+
expect(() =>
544+
transform(`
545+
const [a, b] = $([1, 2])
546+
a = 1
547+
`)
548+
).toThrow(`Assignment to constant variable.`)
549+
550+
expect(() =>
551+
transform(`
552+
const foo = $ref(0)
553+
foo++
554+
`)
555+
).toThrow(`Assignment to constant variable.`)
556+
557+
expect(() =>
558+
transform(`
559+
const foo = $ref(0)
560+
bar = foo
561+
`)
562+
).not.toThrow()
563+
})
534564
})

‎packages/reactivity-transform/src/reactivityTransform.ts

+76-39
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export function shouldTransform(src: string): boolean {
3737
return transformCheckRE.test(src)
3838
}
3939

40-
type Scope = Record<string, boolean | 'prop'>
40+
interface Binding {
41+
isConst?: boolean
42+
isProp?: boolean
43+
}
44+
type Scope = Record<string, Binding | false>
4145

4246
export interface RefTransformOptions {
4347
filename?: string
@@ -118,6 +122,7 @@ export function transformAST(
118122
{
119123
local: string // local identifier, may be different
120124
default?: any
125+
isConst?: boolean
121126
}
122127
>
123128
): {
@@ -168,17 +173,20 @@ export function transformAST(
168173
let escapeScope: CallExpression | undefined // inside $$()
169174
const excludedIds = new WeakSet<Identifier>()
170175
const parentStack: Node[] = []
171-
const propsLocalToPublicMap = Object.create(null)
176+
const propsLocalToPublicMap: Record<string, string> = Object.create(null)
172177

173178
if (knownRefs) {
174179
for (const key of knownRefs) {
175-
rootScope[key] = true
180+
rootScope[key] = {}
176181
}
177182
}
178183
if (knownProps) {
179184
for (const key in knownProps) {
180-
const { local } = knownProps[key]
181-
rootScope[local] = 'prop'
185+
const { local, isConst } = knownProps[key]
186+
rootScope[local] = {
187+
isProp: true,
188+
isConst: !!isConst
189+
}
182190
propsLocalToPublicMap[local] = key
183191
}
184192
}
@@ -218,7 +226,7 @@ export function transformAST(
218226
return false
219227
}
220228

221-
function error(msg: string, node: Node) {
229+
function error(msg: string, node: Node): never {
222230
const e = new Error(msg)
223231
;(e as any).node = node
224232
throw e
@@ -229,10 +237,10 @@ export function transformAST(
229237
return `_${msg}`
230238
}
231239

232-
function registerBinding(id: Identifier, isRef = false) {
240+
function registerBinding(id: Identifier, binding?: Binding) {
233241
excludedIds.add(id)
234242
if (currentScope) {
235-
currentScope[id.name] = isRef
243+
currentScope[id.name] = binding ? binding : false
236244
} else {
237245
error(
238246
'registerBinding called without active scope, something is wrong.',
@@ -241,7 +249,8 @@ export function transformAST(
241249
}
242250
}
243251

244-
const registerRefBinding = (id: Identifier) => registerBinding(id, true)
252+
const registerRefBinding = (id: Identifier, isConst = false) =>
253+
registerBinding(id, { isConst })
245254

246255
let tempVarCount = 0
247256
function genTempVar() {
@@ -296,7 +305,12 @@ export function transformAST(
296305
isCall &&
297306
(refCall = isRefCreationCall((decl as any).init.callee.name))
298307
) {
299-
processRefDeclaration(refCall, decl.id, decl.init as CallExpression)
308+
processRefDeclaration(
309+
refCall,
310+
decl.id,
311+
decl.init as CallExpression,
312+
stmt.kind === 'const'
313+
)
300314
} else {
301315
const isProps =
302316
isRoot && isCall && (decl as any).init.callee.name === 'defineProps'
@@ -316,7 +330,8 @@ export function transformAST(
316330
function processRefDeclaration(
317331
method: string,
318332
id: VariableDeclarator['id'],
319-
call: CallExpression
333+
call: CallExpression,
334+
isConst: boolean
320335
) {
321336
excludedIds.add(call.callee as Identifier)
322337
if (method === convertSymbol) {
@@ -325,16 +340,16 @@ export function transformAST(
325340
s.remove(call.callee.start! + offset, call.callee.end! + offset)
326341
if (id.type === 'Identifier') {
327342
// single variable
328-
registerRefBinding(id)
343+
registerRefBinding(id, isConst)
329344
} else if (id.type === 'ObjectPattern') {
330-
processRefObjectPattern(id, call)
345+
processRefObjectPattern(id, call, isConst)
331346
} else if (id.type === 'ArrayPattern') {
332-
processRefArrayPattern(id, call)
347+
processRefArrayPattern(id, call, isConst)
333348
}
334349
} else {
335350
// shorthands
336351
if (id.type === 'Identifier') {
337-
registerRefBinding(id)
352+
registerRefBinding(id, isConst)
338353
// replace call
339354
s.overwrite(
340355
call.start! + offset,
@@ -350,6 +365,7 @@ export function transformAST(
350365
function processRefObjectPattern(
351366
pattern: ObjectPattern,
352367
call: CallExpression,
368+
isConst: boolean,
353369
tempVar?: string,
354370
path: PathSegment[] = []
355371
) {
@@ -384,21 +400,27 @@ export function transformAST(
384400
// { foo: bar }
385401
nameId = p.value
386402
} else if (p.value.type === 'ObjectPattern') {
387-
processRefObjectPattern(p.value, call, tempVar, [...path, key])
403+
processRefObjectPattern(p.value, call, isConst, tempVar, [
404+
...path,
405+
key
406+
])
388407
} else if (p.value.type === 'ArrayPattern') {
389-
processRefArrayPattern(p.value, call, tempVar, [...path, key])
408+
processRefArrayPattern(p.value, call, isConst, tempVar, [
409+
...path,
410+
key
411+
])
390412
} else if (p.value.type === 'AssignmentPattern') {
391413
if (p.value.left.type === 'Identifier') {
392414
// { foo: bar = 1 }
393415
nameId = p.value.left
394416
defaultValue = p.value.right
395417
} else if (p.value.left.type === 'ObjectPattern') {
396-
processRefObjectPattern(p.value.left, call, tempVar, [
418+
processRefObjectPattern(p.value.left, call, isConst, tempVar, [
397419
...path,
398420
[key, p.value.right]
399421
])
400422
} else if (p.value.left.type === 'ArrayPattern') {
401-
processRefArrayPattern(p.value.left, call, tempVar, [
423+
processRefArrayPattern(p.value.left, call, isConst, tempVar, [
402424
...path,
403425
[key, p.value.right]
404426
])
@@ -412,7 +434,7 @@ export function transformAST(
412434
error(`reactivity destructure does not support rest elements.`, p)
413435
}
414436
if (nameId) {
415-
registerRefBinding(nameId)
437+
registerRefBinding(nameId, isConst)
416438
// inject toRef() after original replaced pattern
417439
const source = pathToString(tempVar, path)
418440
const keyStr = isString(key)
@@ -437,6 +459,7 @@ export function transformAST(
437459
function processRefArrayPattern(
438460
pattern: ArrayPattern,
439461
call: CallExpression,
462+
isConst: boolean,
440463
tempVar?: string,
441464
path: PathSegment[] = []
442465
) {
@@ -462,12 +485,12 @@ export function transformAST(
462485
// [...a]
463486
error(`reactivity destructure does not support rest elements.`, e)
464487
} else if (e.type === 'ObjectPattern') {
465-
processRefObjectPattern(e, call, tempVar, [...path, i])
488+
processRefObjectPattern(e, call, isConst, tempVar, [...path, i])
466489
} else if (e.type === 'ArrayPattern') {
467-
processRefArrayPattern(e, call, tempVar, [...path, i])
490+
processRefArrayPattern(e, call, isConst, tempVar, [...path, i])
468491
}
469492
if (nameId) {
470-
registerRefBinding(nameId)
493+
registerRefBinding(nameId, isConst)
471494
// inject toRef() after original replaced pattern
472495
const source = pathToString(tempVar, path)
473496
const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
@@ -520,9 +543,18 @@ export function transformAST(
520543
parentStack: Node[]
521544
): boolean {
522545
if (hasOwn(scope, id.name)) {
523-
const bindingType = scope[id.name]
524-
if (bindingType) {
525-
const isProp = bindingType === 'prop'
546+
const binding = scope[id.name]
547+
548+
if (binding) {
549+
if (
550+
binding.isConst &&
551+
((parent.type === 'AssignmentExpression' && id === parent.left) ||
552+
parent.type === 'UpdateExpression')
553+
) {
554+
error(`Assignment to constant variable.`, id)
555+
}
556+
557+
const { isProp } = binding
526558
if (isStaticProperty(parent) && parent.shorthand) {
527559
// let binding used in a property shorthand
528560
// skip for destructure patterns
@@ -638,18 +670,20 @@ export function transformAST(
638670
return this.skip()
639671
}
640672

641-
if (
642-
node.type === 'Identifier' &&
643-
// if inside $$(), skip unless this is a destructured prop binding
644-
!(escapeScope && rootScope[node.name] !== 'prop') &&
645-
isReferencedIdentifier(node, parent!, parentStack) &&
646-
!excludedIds.has(node)
647-
) {
648-
// walk up the scope chain to check if id should be appended .value
649-
let i = scopeStack.length
650-
while (i--) {
651-
if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
652-
return
673+
if (node.type === 'Identifier') {
674+
const binding = rootScope[node.name]
675+
if (
676+
// if inside $$(), skip unless this is a destructured prop binding
677+
!(escapeScope && (!binding || !binding.isProp)) &&
678+
isReferencedIdentifier(node, parent!, parentStack) &&
679+
!excludedIds.has(node)
680+
) {
681+
// walk up the scope chain to check if id should be appended .value
682+
let i = scopeStack.length
683+
while (i--) {
684+
if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
685+
return
686+
}
653687
}
654688
}
655689
}
@@ -729,7 +763,10 @@ export function transformAST(
729763
})
730764

731765
return {
732-
rootRefs: Object.keys(rootScope).filter(key => rootScope[key] === true),
766+
rootRefs: Object.keys(rootScope).filter(key => {
767+
const binding = rootScope[key]
768+
return binding && !binding.isProp
769+
}),
733770
importedHelpers: [...importedHelpers]
734771
}
735772
}

0 commit comments

Comments
 (0)
Please sign in to comment.