Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 684a3d2

Browse files
rzzfFloEdelmannwaynzh
authored
feat: add vue/no-undef-directives rule (#2990)
Co-authored-by: Flo Edelmann <git@flo-edelmann.de> Co-authored-by: Wayne Zhang <waynzh19@gmail.com>
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)

AltStyle によって変換されたページ (->オリジナル) /