Skip to content

Commit eee044e

Browse files
committed
Update vue/no-setup-props-destructure rule to support <script setup>
1 parent 774056c commit eee044e

File tree

2 files changed

+225
-70
lines changed

2 files changed

+225
-70
lines changed

lib/rules/no-setup-props-destructure.js

Lines changed: 133 additions & 70 deletions
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
}

tests/lib/rules/no-setup-props-destructure.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,41 @@ 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+
const {count} = props
175+
</script>
176+
`,
177+
errors: [
178+
{
179+
messageId: 'getProperty',
180+
line: 4
181+
}
182+
]
183+
},
184+
{
185+
filename: 'test.vue',
186+
code: `
187+
<script setup>
188+
const props = defineProps({count:Number})
189+
watch(() => {
190+
({ count } = props)
191+
})
192+
watch(() => {
193+
count = props.count
194+
})
195+
</script>
196+
`,
197+
errors: [
198+
{
199+
messageId: 'getProperty',
200+
line: 4
201+
}
202+
]
168203
}
169204
],
170205
invalid: [
@@ -428,6 +463,63 @@ tester.run('no-setup-props-destructure', rule, {
428463
line: 5
429464
}
430465
]
466+
},
467+
{
468+
filename: 'test.vue',
469+
code: `
470+
<script setup>
471+
const {count} = defineProps({count:Number})
472+
</script>
473+
`,
474+
errors: [
475+
{
476+
message:
477+
'Destructuring the `props` will cause the value to lose reactivity.',
478+
line: 3
479+
}
480+
]
481+
},
482+
{
483+
filename: 'test.vue',
484+
code: `
485+
<script setup>
486+
const props = defineProps({count:Number})
487+
const {count} = props
488+
;({count} = props)
489+
</script>
490+
`,
491+
errors: [
492+
{
493+
message:
494+
'Getting a value from the `props` in root scope of `<script setup>` will cause the value to lose reactivity.',
495+
line: 4
496+
},
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: 5
501+
}
502+
]
503+
},
504+
{
505+
filename: 'test.vue',
506+
code: `
507+
<script setup>
508+
const props = defineProps({count:Number})
509+
const count = props.count
510+
count = props.count
511+
</script>
512+
`,
513+
errors: [
514+
{
515+
messageId: 'getProperty',
516+
line: 4
517+
},
518+
{
519+
messageId: 'getProperty',
520+
line: 5
521+
}
522+
]
431523
}
432524
]
433525
})

0 commit comments

Comments
 (0)