Skip to content

Commit 994d74e

Browse files
authored
Update vue/no-setup-props-destructure rule to support <script setup> (#1540)
* Update `vue/no-setup-props-destructure` rule to support `<script setup>` * fix test
1 parent 9c8f293 commit 994d74e

File tree

2 files changed

+230
-70
lines changed

2 files changed

+230
-70
lines changed

Diff for: lib/rules/no-setup-props-destructure.js

+133-70
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,40 @@ module.exports = {
2020
destructuring:
2121
'Destructuring the `props` will cause the value to lose reactivity.',
2222
getProperty:
23-
'Getting a value from the `props` in root scope of `setup()` will cause the value to lose reactivity.'
23+
'Getting a value from the `props` in root scope of `{{scopeName}}` will cause the value to lose reactivity.'
2424
}
2525
},
2626
/** @param {RuleContext} context */
2727
create(context) {
28-
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, Set<Identifier>>} */
28+
/**
29+
* @typedef {object} ScopePropsReferences
30+
* @property {Set<Identifier>} refs
31+
* @property {string} scopeName
32+
*/
33+
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program, ScopePropsReferences>} */
2934
const setupScopePropsReferenceIds = new Map()
3035

3136
/**
3237
* @param {ESNode} node
3338
* @param {string} messageId
39+
* @param {string} scopeName
3440
*/
35-
function report(node, messageId) {
41+
function report(node, messageId, scopeName) {
3642
context.report({
3743
node,
38-
messageId
44+
messageId,
45+
data: {
46+
scopeName
47+
}
3948
})
4049
}
4150

4251
/**
4352
* @param {Pattern} left
4453
* @param {Expression | null} right
45-
* @param {Set<Identifier>} propsReferenceIds
54+
* @param {ScopePropsReferences} propsReferences
4655
*/
47-
function verify(left, right, propsReferenceIds) {
56+
function verify(left, right, propsReferences) {
4857
if (!right) {
4958
return
5059
}
@@ -62,88 +71,142 @@ module.exports = {
6271
while (rightId.type === 'MemberExpression') {
6372
rightId = utils.skipChainExpression(rightId.object)
6473
}
65-
if (rightId.type === 'Identifier' && propsReferenceIds.has(rightId)) {
66-
report(left, 'getProperty')
74+
if (rightId.type === 'Identifier' && propsReferences.refs.has(rightId)) {
75+
report(left, 'getProperty', propsReferences.scopeName)
6776
}
6877
}
6978
/**
7079
* @typedef {object} ScopeStack
7180
* @property {ScopeStack | null} upper
72-
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} functionNode
81+
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
7382
*/
7483
/**
7584
* @type {ScopeStack | null}
7685
*/
7786
let scopeStack = null
7887

79-
return utils.defineVueVisitor(context, {
80-
':function'(node) {
81-
scopeStack = {
82-
upper: scopeStack,
83-
functionNode: node
84-
}
85-
},
86-
onSetupFunctionEnter(node) {
87-
const propsParam = utils.skipDefaultParamValue(node.params[0])
88-
if (!propsParam) {
89-
// no arguments
90-
return
91-
}
92-
if (propsParam.type === 'RestElement') {
93-
// cannot check
94-
return
95-
}
96-
if (
97-
propsParam.type === 'ArrayPattern' ||
98-
propsParam.type === 'ObjectPattern'
99-
) {
100-
report(propsParam, 'destructuring')
101-
return
102-
}
88+
/**
89+
* @param {Pattern | null} node
90+
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
91+
* @param {string} scopeName
92+
*/
93+
function processPattern(node, scopeNode, scopeName) {
94+
if (!node) {
95+
// no arguments
96+
return
97+
}
98+
if (
99+
node.type === 'RestElement' ||
100+
node.type === 'AssignmentPattern' ||
101+
node.type === 'MemberExpression'
102+
) {
103+
// cannot check
104+
return
105+
}
106+
if (node.type === 'ArrayPattern' || node.type === 'ObjectPattern') {
107+
report(node, 'destructuring', scopeName)
108+
return
109+
}
103110

104-
const variable = findVariable(context.getScope(), propsParam)
105-
if (!variable) {
106-
return
111+
const variable = findVariable(context.getScope(), node)
112+
if (!variable) {
113+
return
114+
}
115+
const propsReferenceIds = new Set()
116+
for (const reference of variable.references) {
117+
if (!reference.isRead()) {
118+
continue
107119
}
108-
const propsReferenceIds = new Set()
109-
for (const reference of variable.references) {
110-
if (!reference.isRead()) {
111-
continue
120+
121+
propsReferenceIds.add(reference.identifier)
122+
}
123+
setupScopePropsReferenceIds.set(scopeNode, {
124+
refs: propsReferenceIds,
125+
scopeName
126+
})
127+
}
128+
return utils.compositingVisitors(
129+
{
130+
/**
131+
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | Program} node
132+
*/
133+
'Program, :function'(node) {
134+
scopeStack = {
135+
upper: scopeStack,
136+
scopeNode: node
112137
}
138+
},
139+
/**
140+
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | Program} node
141+
*/
142+
'Program, :function:exit'(node) {
143+
scopeStack = scopeStack && scopeStack.upper
113144

114-
propsReferenceIds.add(reference.identifier)
115-
}
116-
setupScopePropsReferenceIds.set(node, propsReferenceIds)
117-
},
118-
VariableDeclarator(node) {
119-
if (!scopeStack) {
120-
return
121-
}
122-
const propsReferenceIds = setupScopePropsReferenceIds.get(
123-
scopeStack.functionNode
124-
)
125-
if (!propsReferenceIds) {
126-
return
145+
setupScopePropsReferenceIds.delete(node)
146+
},
147+
/**
148+
* @param {VariableDeclarator} node
149+
*/
150+
VariableDeclarator(node) {
151+
if (!scopeStack) {
152+
return
153+
}
154+
const propsReferenceIds = setupScopePropsReferenceIds.get(
155+
scopeStack.scopeNode
156+
)
157+
if (!propsReferenceIds) {
158+
return
159+
}
160+
verify(node.id, node.init, propsReferenceIds)
161+
},
162+
/**
163+
* @param {AssignmentExpression} node
164+
*/
165+
AssignmentExpression(node) {
166+
if (!scopeStack) {
167+
return
168+
}
169+
const propsReferenceIds = setupScopePropsReferenceIds.get(
170+
scopeStack.scopeNode
171+
)
172+
if (!propsReferenceIds) {
173+
return
174+
}
175+
verify(node.left, node.right, propsReferenceIds)
127176
}
128-
verify(node.id, node.init, propsReferenceIds)
129177
},
130-
AssignmentExpression(node) {
131-
if (!scopeStack) {
132-
return
178+
utils.defineScriptSetupVisitor(context, {
179+
onDefinePropsEnter(node) {
180+
let target = node
181+
if (
182+
target.parent &&
183+
target.parent.type === 'CallExpression' &&
184+
target.parent.arguments[0] === target &&
185+
target.parent.callee.type === 'Identifier' &&
186+
target.parent.callee.name === 'withDefaults'
187+
) {
188+
target = target.parent
189+
}
190+
if (!target.parent) {
191+
return
192+
}
193+
194+
/** @type {Pattern|null} */
195+
let id = null
196+
if (target.parent.type === 'VariableDeclarator') {
197+
id = target.parent.init === target ? target.parent.id : null
198+
} else if (target.parent.type === 'AssignmentExpression') {
199+
id = target.parent.right === target ? target.parent.left : null
200+
}
201+
processPattern(id, context.getSourceCode().ast, '<script setup>')
133202
}
134-
const propsReferenceIds = setupScopePropsReferenceIds.get(
135-
scopeStack.functionNode
136-
)
137-
if (!propsReferenceIds) {
138-
return
203+
}),
204+
utils.defineVueVisitor(context, {
205+
onSetupFunctionEnter(node) {
206+
const propsParam = utils.skipDefaultParamValue(node.params[0])
207+
processPattern(propsParam, node, 'setup()')
139208
}
140-
verify(node.left, node.right, propsReferenceIds)
141-
},
142-
':function:exit'(node) {
143-
scopeStack = scopeStack && scopeStack.upper
144-
145-
setupScopePropsReferenceIds.delete(node)
146-
}
147-
})
209+
})
210+
)
148211
}
149212
}

Diff for: tests/lib/rules/no-setup-props-destructure.js

+97
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,46 @@ tester.run('no-setup-props-destructure', rule, {
165165
Vue.component('test', {
166166
el: a = b
167167
})`
168+
},
169+
{
170+
filename: 'test.vue',
171+
code: `
172+
<script setup>
173+
const props = defineProps({count:Number})
174+
watch(() => {
175+
const {count} = props
176+
})
177+
watch(() => {
178+
const count = props.count
179+
})
180+
</script>
181+
`,
182+
errors: [
183+
{
184+
messageId: 'getProperty',
185+
line: 4
186+
}
187+
]
188+
},
189+
{
190+
filename: 'test.vue',
191+
code: `
192+
<script setup>
193+
const props = defineProps({count:Number})
194+
watch(() => {
195+
({ count } = props)
196+
})
197+
watch(() => {
198+
count = props.count
199+
})
200+
</script>
201+
`,
202+
errors: [
203+
{
204+
messageId: 'getProperty',
205+
line: 4
206+
}
207+
]
168208
}
169209
],
170210
invalid: [
@@ -428,6 +468,63 @@ tester.run('no-setup-props-destructure', rule, {
428468
line: 5
429469
}
430470
]
471+
},
472+
{
473+
filename: 'test.vue',
474+
code: `
475+
<script setup>
476+
const {count} = defineProps({count:Number})
477+
</script>
478+
`,
479+
errors: [
480+
{
481+
message:
482+
'Destructuring the `props` will cause the value to lose reactivity.',
483+
line: 3
484+
}
485+
]
486+
},
487+
{
488+
filename: 'test.vue',
489+
code: `
490+
<script setup>
491+
const props = defineProps({count:Number})
492+
const {count} = props
493+
;({count} = props)
494+
</script>
495+
`,
496+
errors: [
497+
{
498+
message:
499+
'Getting a value from the `props` in root scope of `<script setup>` will cause the value to lose reactivity.',
500+
line: 4
501+
},
502+
{
503+
message:
504+
'Getting a value from the `props` in root scope of `<script setup>` will cause the value to lose reactivity.',
505+
line: 5
506+
}
507+
]
508+
},
509+
{
510+
filename: 'test.vue',
511+
code: `
512+
<script setup>
513+
const props = defineProps({count:Number})
514+
const count = props.count
515+
count = props.count
516+
</script>
517+
`,
518+
errors: [
519+
{
520+
messageId: 'getProperty',
521+
line: 4
522+
},
523+
{
524+
messageId: 'getProperty',
525+
line: 5
526+
}
527+
]
431528
}
432529
]
433530
})

0 commit comments

Comments
 (0)