Skip to content

Commit 684a3d2

Browse files
rzzfFloEdelmannwaynzh
authored
feat: add vue/no-undef-directives rule (#2990)
Co-authored-by: Flo Edelmann <[email protected]> Co-authored-by: Wayne Zhang <[email protected]>
1 parent 58432ae commit 684a3d2

File tree

6 files changed

+822
-0
lines changed

6 files changed

+822
-0
lines changed

.changeset/fast-monkeys-smoke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-vue": minor
3+
---
4+
5+
Added new `vue/no-undef-directives` rule

docs/rules/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ For example:
258258
| [vue/no-template-target-blank] | disallow target="_blank" attribute without rel="noopener noreferrer" | :bulb: | :warning: |
259259
| [vue/no-this-in-before-route-enter] | disallow `this` usage in a `beforeRouteEnter` method | | :warning: |
260260
| [vue/no-undef-components] | disallow use of undefined components in `<template>` | | :hammer: |
261+
| [vue/no-undef-directives] | disallow use of undefined custom directives | | :hammer: |
261262
| [vue/no-undef-properties] | disallow undefined properties | | :hammer: |
262263
| [vue/no-unsupported-features] | disallow unsupported Vue.js syntax on the specified version | :wrench: | :hammer: |
263264
| [vue/no-unused-emit-declarations] | disallow unused emit declarations | | :hammer: |
@@ -521,6 +522,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
521522
[vue/no-textarea-mustache]: ./no-textarea-mustache.md
522523
[vue/no-this-in-before-route-enter]: ./no-this-in-before-route-enter.md
523524
[vue/no-undef-components]: ./no-undef-components.md
525+
[vue/no-undef-directives]: ./no-undef-directives.md
524526
[vue/no-undef-properties]: ./no-undef-properties.md
525527
[vue/no-unsupported-features]: ./no-unsupported-features.md
526528
[vue/no-unused-components]: ./no-unused-components.md

docs/rules/no-undef-directives.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-undef-directives
5+
description: disallow use of undefined custom directives
6+
---
7+
8+
# vue/no-undef-directives
9+
10+
> disallow use of undefined custom directives
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
13+
14+
## :book: Rule Details
15+
16+
This rule reports directives that are used in the `<template>`, but that are not registered in the `<script setup>` or the Options API's `directives` section.
17+
18+
Undefined directives will be resolved from globally registered directives. However, if you are not using global directives, you can use this rule to prevent runtime errors.
19+
20+
<eslint-code-block :rules="{'vue/no-undef-directives': ['error']}">
21+
22+
```vue
23+
<script setup>
24+
import vFocus from './vFocus';
25+
</script>
26+
27+
<template>
28+
<!-- ✓ GOOD -->
29+
<input v-focus>
30+
31+
<!-- ✗ BAD -->
32+
<div v-foo></div>
33+
</template>
34+
```
35+
36+
</eslint-code-block>
37+
38+
<eslint-code-block :rules="{'vue/no-undef-directives': ['error']}">
39+
40+
```vue
41+
<template>
42+
<!-- ✓ GOOD -->
43+
<input v-focus>
44+
45+
<!-- ✗ BAD -->
46+
<div v-foo></div>
47+
</template>
48+
49+
<script>
50+
import vFocus from './vFocus';
51+
52+
export default {
53+
directives: {
54+
focus: vFocus
55+
}
56+
}
57+
</script>
58+
```
59+
60+
</eslint-code-block>
61+
62+
## :wrench: Options
63+
64+
```json
65+
{
66+
"vue/no-undef-directives": ["error", {
67+
"ignore": ["foo"]
68+
}]
69+
}
70+
```
71+
72+
- `"ignore"` (`string[]`) An array of directive names or regular expression patterns (e.g. `"/^custom-/"`) that ignore these rules. This option will check both kebab-case and PascalCase versions of the given directive names. Default is empty.
73+
74+
### `"ignore": ["foo"]`
75+
76+
<eslint-code-block :rules="{'vue/no-undef-directives': ['error', {ignore: ['foo']}]}">
77+
78+
```vue
79+
<template>
80+
<!-- ✓ GOOD -->
81+
<div v-foo></div>
82+
</template>
83+
```
84+
85+
</eslint-code-block>
86+
87+
## :mag: Implementation
88+
89+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-undef-directives.js)
90+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-undef-directives.js)

lib/plugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ const plugin = {
151151
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
152152
'no-this-in-before-route-enter': require('./rules/no-this-in-before-route-enter'),
153153
'no-undef-components': require('./rules/no-undef-components'),
154+
'no-undef-directives': require('./rules/no-undef-directives'),
154155
'no-undef-properties': require('./rules/no-undef-properties'),
155156
'no-unsupported-features': require('./rules/no-unsupported-features'),
156157
'no-unused-components': require('./rules/no-unused-components'),

lib/rules/no-undef-directives.js

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/**
2+
* @author rzzf
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
const casing = require('../utils/casing')
9+
const regexp = require('../utils/regexp')
10+
11+
/**
12+
* @param {ObjectExpression} componentObject
13+
* @returns { { node: Property, name: string }[] } Array of ASTNodes
14+
*/
15+
function getRegisteredDirectives(componentObject) {
16+
const directivesNode = componentObject.properties.find(
17+
(p) =>
18+
p.type === 'Property' &&
19+
utils.getStaticPropertyName(p) === 'directives' &&
20+
p.value.type === 'ObjectExpression'
21+
)
22+
23+
if (
24+
!directivesNode ||
25+
directivesNode.type !== 'Property' ||
26+
directivesNode.value.type !== 'ObjectExpression'
27+
) {
28+
return []
29+
}
30+
31+
return directivesNode.value.properties.flatMap((node) => {
32+
const name =
33+
node.type === 'Property' ? utils.getStaticPropertyName(node) : null
34+
return name ? [{ node: /** @type {Property} */ (node), name }] : []
35+
})
36+
}
37+
38+
/**
39+
* @param {string} rawName
40+
* @param {Set<string>} definedNames
41+
*/
42+
function isDefinedInSetup(rawName, definedNames) {
43+
const camelName = casing.camelCase(rawName)
44+
const variableName = `v${casing.capitalize(camelName)}`
45+
return definedNames.has(variableName)
46+
}
47+
48+
/**
49+
* @param {string} rawName
50+
* @param {Set<string>} definedNames
51+
*/
52+
function isDefinedInOptions(rawName, definedNames) {
53+
const camelName = casing.camelCase(rawName)
54+
55+
if (definedNames.has(rawName)) {
56+
return true
57+
}
58+
59+
// allow case-insensitive only when the directive name itself contains capitalized letters
60+
for (const name of definedNames) {
61+
const lowercaseName = name.toLowerCase()
62+
if (name !== lowercaseName && lowercaseName === camelName.toLowerCase()) {
63+
return true
64+
}
65+
}
66+
67+
return false
68+
}
69+
70+
module.exports = {
71+
meta: {
72+
type: 'suggestion',
73+
docs: {
74+
description: 'disallow use of undefined custom directives',
75+
categories: undefined,
76+
url: 'https://eslint.vuejs.org/rules/no-undef-directives.html'
77+
},
78+
fixable: null,
79+
schema: [
80+
{
81+
type: 'object',
82+
properties: {
83+
ignore: {
84+
type: 'array',
85+
items: { type: 'string' },
86+
uniqueItems: true
87+
}
88+
},
89+
additionalProperties: false
90+
}
91+
],
92+
messages: {
93+
undef: "The 'v-{{name}}' directive has been used, but not defined."
94+
}
95+
},
96+
/** @param {RuleContext} context */
97+
create(context) {
98+
const options = context.options[0] || {}
99+
const { ignore = [] } = options
100+
const isAnyIgnored = regexp.toRegExpGroupMatcher(ignore)
101+
102+
/**
103+
* Check whether the given directive name is a verify target or not.
104+
*
105+
* @param {string} rawName The directive name.
106+
* @returns {boolean}
107+
*/
108+
function isVerifyTargetDirective(rawName) {
109+
const kebabName = casing.kebabCase(rawName)
110+
if (
111+
utils.isBuiltInDirectiveName(rawName) ||
112+
isAnyIgnored(rawName, kebabName)
113+
) {
114+
return false
115+
}
116+
return true
117+
}
118+
119+
/**
120+
* @param {(rawName: string) => boolean} isDefined
121+
* @returns {TemplateListener}
122+
*/
123+
function createTemplateBodyVisitor(isDefined) {
124+
return {
125+
/** @param {VDirective} node */
126+
'VAttribute[directive=true]'(node) {
127+
const name = node.key.name.name
128+
if (utils.isBuiltInDirectiveName(name)) {
129+
return
130+
}
131+
const rawName = node.key.name.rawName || name
132+
if (isVerifyTargetDirective(rawName) && !isDefined(rawName)) {
133+
context.report({
134+
node: node.key,
135+
messageId: 'undef',
136+
data: {
137+
name: rawName
138+
}
139+
})
140+
}
141+
}
142+
}
143+
}
144+
145+
/** @type {Set<string>} */
146+
const definedInOptionDirectives = new Set()
147+
148+
if (utils.isScriptSetup(context)) {
149+
// For <script setup>
150+
/** @type {Set<string>} */
151+
const definedInSetupDirectives = new Set()
152+
153+
const globalScope = context.sourceCode.scopeManager.globalScope
154+
if (globalScope) {
155+
for (const variable of globalScope.variables) {
156+
definedInSetupDirectives.add(variable.name)
157+
}
158+
const moduleScope = globalScope.childScopes.find(
159+
(scope) => scope.type === 'module'
160+
)
161+
for (const variable of moduleScope?.variables ?? []) {
162+
definedInSetupDirectives.add(variable.name)
163+
}
164+
}
165+
166+
const scriptVisitor = utils.defineVueVisitor(context, {
167+
onVueObjectEnter(node) {
168+
for (const directive of getRegisteredDirectives(node)) {
169+
definedInOptionDirectives.add(directive.name)
170+
}
171+
}
172+
})
173+
174+
const templateBodyVisitor = createTemplateBodyVisitor(
175+
(rawName) =>
176+
isDefinedInSetup(rawName, definedInSetupDirectives) ||
177+
isDefinedInOptions(rawName, definedInOptionDirectives)
178+
)
179+
180+
return utils.defineTemplateBodyVisitor(
181+
context,
182+
templateBodyVisitor,
183+
scriptVisitor
184+
)
185+
}
186+
187+
// For Options API
188+
const scriptVisitor = utils.executeOnVue(context, (obj) => {
189+
for (const directive of getRegisteredDirectives(obj)) {
190+
definedInOptionDirectives.add(directive.name)
191+
}
192+
})
193+
194+
const templateBodyVisitor = createTemplateBodyVisitor((rawName) =>
195+
isDefinedInOptions(rawName, definedInOptionDirectives)
196+
)
197+
198+
return utils.defineTemplateBodyVisitor(
199+
context,
200+
templateBodyVisitor,
201+
scriptVisitor
202+
)
203+
}
204+
}

0 commit comments

Comments
 (0)