Skip to content

Commit d3cc998

Browse files
committed
feat: add no-deprecated-slots rule
closes #28
1 parent 66f45ca commit d3cc998

File tree

4 files changed

+418
-0
lines changed

4 files changed

+418
-0
lines changed

docs/rules/no-deprecated-slots.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Prevent the use of removed slot variables (no-deprecated-slots)
2+
3+
:wrench: This rule is partially fixable with `eslint --fix`
4+
5+
## Rule Details
6+
7+
Examples of **incorrect** code for this rule:
8+
9+
```html
10+
<v-dialog>
11+
<template #activator="{ on }">
12+
<v-btn v-on="on" />
13+
</template>
14+
</v-dialog>
15+
```
16+
17+
Examples of **correct** code for this rule:
18+
19+
```html
20+
<v-dialog>
21+
<template #activator="{ props }">
22+
<v-btn v-bind="props" />
23+
</template>
24+
</v-dialog>
25+
```
26+
27+
Variable shadowing is not currently handled, the following will produce incorrect output that must be fixed manually:
28+
29+
```html
30+
<v-menu>
31+
<template #activator="{ on: menuOn }">
32+
<v-tooltip>
33+
<template #activator="{ on: tooltipOn }">
34+
<v-btn v-on="{ ...tooltipOn, ...menuOn }" />
35+
</template>
36+
</v-tooltip>
37+
</template>
38+
</v-menu>
39+
```
40+
41+
### Options
42+
43+
This rule has no configuration options.

src/configs/base.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ module.exports = {
1414
'vuetify/no-deprecated-components': 'error',
1515
'vuetify/no-deprecated-events': 'error',
1616
'vuetify/no-deprecated-props': 'error',
17+
'vuetify/no-deprecated-slots': 'error',
1718
},
1819
}

src/rules/no-deprecated-slots.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
'use strict'
2+
3+
// ------------------------------------------------------------------------------
4+
// Rule Definition
5+
// ------------------------------------------------------------------------------
6+
7+
module.exports = {
8+
meta: {
9+
docs: {
10+
description: 'Prevent the use of removed and deprecated slots.',
11+
category: 'recommended',
12+
},
13+
fixable: 'code',
14+
schema: [],
15+
messages: {
16+
changedProps: `{{ component }}'s '{{ slot }}' slot has changed props`,
17+
invalidProps: `Slot has invalid props`,
18+
},
19+
},
20+
21+
create (context) {
22+
const template = context.parserServices.getTemplateBodyTokenStore()
23+
let scopeStack
24+
25+
return context.parserServices.defineTemplateBodyVisitor({
26+
VElement (node) {
27+
scopeStack = {
28+
parent: scopeStack,
29+
nodes: scopeStack ? [...scopeStack.nodes] : [],
30+
}
31+
for (const variable of node.variables) {
32+
scopeStack.nodes.push(variable.id)
33+
}
34+
35+
if (node.name !== 'template') return
36+
if (!['v-dialog', 'v-menu', 'v-tooltip'].includes(node.parent.name)) return
37+
38+
const directive = node.startTag.attributes.find(attr => {
39+
return (
40+
attr.directive &&
41+
attr.key.name.name === 'slot' &&
42+
attr.key.argument?.name === 'activator'
43+
)
44+
})
45+
46+
if (
47+
!directive ||
48+
!directive.value ||
49+
directive.value.type !== 'VExpressionContainer' ||
50+
!directive.value.expression ||
51+
directive.value.expression.params.length !== 1
52+
) return
53+
54+
const param = directive.value.expression.params[0]
55+
if (param.type === 'Identifier') {
56+
// #activator="data"
57+
const boundVariables = {}
58+
node.variables.find(variable => variable.id.name === param.name)?.references.forEach(ref => {
59+
if (ref.id.parent.type !== 'MemberExpression') return
60+
if (
61+
// v-bind="data.props"
62+
ref.id.parent.property.name === 'props' &&
63+
ref.id.parent.parent.parent.directive &&
64+
ref.id.parent.parent.parent.key.name.name === 'bind' &&
65+
!ref.id.parent.parent.parent.key.argument
66+
) return
67+
if (ref.id.parent.property.name === 'on') {
68+
boundVariables.on = ref.id
69+
} else if (ref.id.parent.property.name === 'attrs') {
70+
boundVariables.attrs = ref.id
71+
}
72+
})
73+
if (boundVariables.on) {
74+
const ref = boundVariables.on
75+
context.report({
76+
node: ref,
77+
messageId: 'changedProps',
78+
data: {
79+
component: node.parent.name,
80+
slot: directive.key.argument.name,
81+
},
82+
fix (fixer) {
83+
return fixer.replaceText(ref.parent.parent.parent, `v-bind="${param.name}.props"`)
84+
},
85+
})
86+
}
87+
if (boundVariables.attrs) {
88+
const ref = boundVariables.attrs
89+
if (!boundVariables.on) {
90+
context.report({
91+
node: boundVariables.attrs,
92+
messageId: 'invalidProps',
93+
})
94+
} else {
95+
context.report({
96+
node: ref,
97+
messageId: 'changedProps',
98+
data: {
99+
component: node.parent.name,
100+
slot: directive.key.argument.name,
101+
},
102+
fix (fixer) {
103+
return fixer.removeRange([ref.parent.parent.parent.range[0] - 1, ref.parent.parent.parent.range[1]])
104+
},
105+
})
106+
}
107+
}
108+
} else if (param.type === 'ObjectPattern') {
109+
// #activator="{ on, attrs }"
110+
const boundVariables = {}
111+
param.properties.forEach(prop => {
112+
node.variables.find(variable => variable.id.name === prop.value.name)?.references.forEach(ref => {
113+
if (prop.key.name === 'on') {
114+
boundVariables.on = { prop, id: ref.id }
115+
} else if (prop.key.name === 'attrs') {
116+
boundVariables.attrs = { prop, id: ref.id }
117+
}
118+
})
119+
})
120+
if (boundVariables.on || boundVariables.attrs) {
121+
if (boundVariables.attrs && !boundVariables.on) {
122+
context.report({
123+
node: boundVariables.attrs.prop.key,
124+
messageId: 'invalidProps',
125+
})
126+
} else {
127+
context.report({
128+
node: param,
129+
messageId: 'changedProps',
130+
data: {
131+
component: node.parent.name,
132+
slot: directive.key.argument.name,
133+
},
134+
* fix (fixer) {
135+
if (boundVariables.on) {
136+
const ref = boundVariables.on
137+
yield fixer.replaceText(ref.prop, 'props')
138+
yield fixer.replaceText(ref.id.parent.parent, `v-bind="props"`)
139+
}
140+
if (boundVariables.attrs) {
141+
const ref = boundVariables.attrs
142+
const isLast = ref.prop === param.properties.at(-1)
143+
if (isLast) {
144+
const comma = template.getTokenBefore(ref.prop, { filter: token => token.value === ',' })
145+
if (comma) {
146+
yield fixer.removeRange([comma.start, ref.prop.end])
147+
} else {
148+
yield fixer.removeRange([ref.prop.start - 1, ref.prop.end])
149+
}
150+
} else {
151+
const comma = template.getTokenAfter(ref.prop, { filter: token => token.value === ',' })
152+
if (comma) {
153+
yield fixer.removeRange([ref.prop.start - 1, comma.end])
154+
} else {
155+
yield fixer.removeRange([ref.prop.start - 1, ref.prop.end])
156+
}
157+
}
158+
yield fixer.removeRange([ref.id.parent.parent.range[0] - 1, ref.id.parent.parent.range[1]])
159+
}
160+
},
161+
})
162+
}
163+
}
164+
} else {
165+
context.report({
166+
node: directive,
167+
messageId: 'invalidProps',
168+
})
169+
}
170+
},
171+
'VElement:exit' () {
172+
scopeStack = scopeStack && scopeStack.parent
173+
},
174+
})
175+
},
176+
}

0 commit comments

Comments
 (0)