WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 5ab0de6

Browse files
committed
feat: add vue/no-undef-directives rule
1 parent 5023e75 commit 5ab0de6

File tree

6 files changed

+793
-0
lines changed

6 files changed

+793
-0
lines changed

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 | | :warning: |
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: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 use of undefined custom directives in `<template>`.
17+
18+
<eslint-code-block :rules="{'vue/no-undef-directives': ['error']}">
19+
20+
```vue
21+
<template>
22+
<!-- ✗ BAD -->
23+
<input v-focus>
24+
<div v-foo></div>
25+
</template>
26+
27+
<script setup>
28+
// vFocus is not imported
29+
</script>
30+
```
31+
32+
</eslint-code-block>
33+
34+
<eslint-code-block :rules="{'vue/no-undef-directives': ['error']}">
35+
36+
```vue
37+
<template>
38+
<!-- ✓ GOOD -->
39+
<input v-focus>
40+
<div v-foo></div>
41+
</template>
42+
43+
<script setup>
44+
import vFocus from './vFocus';
45+
const vFoo = {}
46+
</script>
47+
```
48+
49+
</eslint-code-block>
50+
51+
## :wrench: Options
52+
53+
```json
54+
{
55+
"vue/no-undef-directives": ["error", {
56+
"ignorePatterns": ["foo"]
57+
}]
58+
}
59+
```
60+
61+
- `ignorePatterns` (`string[]`) ... An array of regex pattern strings to ignore.
62+
63+
### `ignorePatterns`
64+
65+
<eslint-code-block :rules="{'vue/no-undef-directives': ['error', {ignorePatterns: ['foo']}]}">
66+
67+
```vue
68+
<template>
69+
<div v-foo></div>
70+
</template>
71+
```
72+
73+
</eslint-code-block>
74+
75+
## :mag: Implementation
76+
77+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-undef-directives.js)
78+
- [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: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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+
10+
/**
11+
* @param {string} str
12+
* @returns {string}
13+
*/
14+
function camelize(str) {
15+
return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
16+
}
17+
18+
/**
19+
* @param {ObjectExpression} componentObject
20+
* @returns { { node: Property, name: string }[] } Array of ASTNodes
21+
*/
22+
function getRegisteredDirectives(componentObject) {
23+
const directivesNode = componentObject.properties.find(
24+
(p) =>
25+
p.type === 'Property' &&
26+
utils.getStaticPropertyName(p) === 'directives' &&
27+
p.value.type === 'ObjectExpression'
28+
)
29+
30+
if (
31+
!directivesNode ||
32+
directivesNode.type !== 'Property' ||
33+
directivesNode.value.type !== 'ObjectExpression'
34+
) {
35+
return []
36+
}
37+
38+
return directivesNode.value.properties
39+
.filter(
40+
/**
41+
* @param {Property | SpreadElement} node
42+
* @returns {node is Property}
43+
*/
44+
(node) => node.type === 'Property'
45+
)
46+
.map((node) => {
47+
const name = utils.getStaticPropertyName(node)
48+
return name ? { node, name } : null
49+
})
50+
.filter(
51+
/**
52+
* @param {{node: Property, name: string} | null} res
53+
* @returns {res is {node: Property, name: string}}
54+
*/
55+
(res) => res !== null
56+
)
57+
}
58+
59+
class DefinedInSetupDirectives {
60+
constructor() {
61+
/**
62+
* Directive names
63+
* @type {Set<string>}
64+
*/
65+
this.names = new Set()
66+
}
67+
68+
/**
69+
* @param {string[]} names
70+
*/
71+
addName(...names) {
72+
for (const name of names) {
73+
this.names.add(name)
74+
}
75+
}
76+
77+
/**
78+
* @param {string} rawName
79+
*/
80+
isDefinedDirective(rawName) {
81+
const camelName = camelize(rawName)
82+
const variableName = `v${casing.capitalize(camelName)}`
83+
if (this.names.has(variableName)) {
84+
return true
85+
}
86+
return false
87+
}
88+
}
89+
90+
class DefinedInOptionDirectives {
91+
constructor() {
92+
/**
93+
* Directive names
94+
* @type {Set<string>}
95+
*/
96+
this.names = new Set()
97+
}
98+
99+
/**
100+
* @param {string[]} names
101+
*/
102+
addName(...names) {
103+
for (const name of names) {
104+
this.names.add(name)
105+
}
106+
}
107+
108+
/**
109+
* @param {string} rawName
110+
*/
111+
isDefinedDirective(rawName) {
112+
const camelName = camelize(rawName)
113+
if (this.names.has(rawName) || this.names.has(camelName)) {
114+
return true
115+
}
116+
117+
// allow case-insensitive ONLY when the directive name itself contains capitalized letters
118+
for (const name of this.names) {
119+
if (
120+
name.toLowerCase() === camelName.toLowerCase() &&
121+
name !== name.toLowerCase()
122+
) {
123+
return true
124+
}
125+
}
126+
127+
return false
128+
}
129+
}
130+
131+
module.exports = {
132+
meta: {
133+
type: 'problem',
134+
docs: {
135+
description: 'disallow use of undefined custom directives',
136+
categories: undefined,
137+
url: 'https://eslint.vuejs.org/rules/no-undef-directives.html'
138+
},
139+
fixable: null,
140+
schema: [
141+
{
142+
type: 'object',
143+
properties: {
144+
ignorePatterns: {
145+
type: 'array',
146+
items: {
147+
type: 'string'
148+
},
149+
uniqueItems: true
150+
}
151+
},
152+
additionalProperties: true
153+
}
154+
],
155+
messages: {
156+
undef: "The 'v-{{name}}' directive has been used, but not defined."
157+
}
158+
},
159+
/** @param {RuleContext} context */
160+
create(context) {
161+
const options = context.options[0] || {}
162+
/** @type {string[]} */
163+
const ignorePatterns = options.ignorePatterns || []
164+
165+
/**
166+
* Check whether the given directive name is a verify target or not.
167+
*
168+
* @param {string} rawName The directive name.
169+
* @returns {boolean}
170+
*/
171+
function isVerifyTargetDirective(rawName) {
172+
if (utils.isBuiltInDirectiveName(rawName)) {
173+
return false
174+
}
175+
176+
const ignored = ignorePatterns.some((pattern) =>
177+
new RegExp(pattern).test(rawName)
178+
)
179+
return !ignored
180+
}
181+
182+
/** @type { (rawName:string, reportNode: ASTNode) => void } */
183+
let verifyName
184+
/** @type {RuleListener} */
185+
let scriptVisitor = {}
186+
/** @type {TemplateListener} */
187+
const templateBodyVisitor = {
188+
/** @param {VDirective} node */
189+
'VAttribute[directive=true]'(node) {
190+
const name = node.key.name.name
191+
if (utils.isBuiltInDirectiveName(name)) {
192+
return
193+
}
194+
verifyName(node.key.name.rawName || name, node.key)
195+
}
196+
}
197+
198+
if (utils.isScriptSetup(context)) {
199+
// For <script setup>
200+
const definedInSetupDirectives = new DefinedInSetupDirectives()
201+
const definedInOptionDirectives = new DefinedInOptionDirectives()
202+
203+
const globalScope = context.sourceCode.scopeManager.globalScope
204+
if (globalScope) {
205+
for (const variable of globalScope.variables) {
206+
definedInSetupDirectives.addName(variable.name)
207+
}
208+
const moduleScope = globalScope.childScopes.find(
209+
(scope) => scope.type === 'module'
210+
)
211+
for (const variable of (moduleScope && moduleScope.variables) || []) {
212+
definedInSetupDirectives.addName(variable.name)
213+
}
214+
}
215+
216+
scriptVisitor = utils.defineVueVisitor(context, {
217+
onVueObjectEnter(node) {
218+
for (const directive of getRegisteredDirectives(node)) {
219+
definedInOptionDirectives.addName(directive.name)
220+
}
221+
}
222+
})
223+
224+
verifyName = (rawName, reportNode) => {
225+
if (!isVerifyTargetDirective(rawName)) {
226+
return
227+
}
228+
if (definedInSetupDirectives.isDefinedDirective(rawName)) {
229+
return
230+
}
231+
if (definedInOptionDirectives.isDefinedDirective(rawName)) {
232+
return
233+
}
234+
235+
context.report({
236+
node: reportNode,
237+
messageId: 'undef',
238+
data: {
239+
name: rawName
240+
}
241+
})
242+
}
243+
} else {
244+
// For Options API
245+
const definedInOptionDirectives = new DefinedInOptionDirectives()
246+
247+
scriptVisitor = utils.executeOnVue(context, (obj) => {
248+
for (const directive of getRegisteredDirectives(obj)) {
249+
definedInOptionDirectives.addName(directive.name)
250+
}
251+
})
252+
253+
verifyName = (rawName, reportNode) => {
254+
if (!isVerifyTargetDirective(rawName)) {
255+
return
256+
}
257+
if (definedInOptionDirectives.isDefinedDirective(rawName)) {
258+
return
259+
}
260+
261+
context.report({
262+
node: reportNode,
263+
messageId: 'undef',
264+
data: {
265+
name: rawName
266+
}
267+
})
268+
}
269+
}
270+
271+
return utils.defineTemplateBodyVisitor(
272+
context,
273+
templateBodyVisitor,
274+
scriptVisitor
275+
)
276+
}
277+
}

tests/lib/rules/no-undef-directives-repro.js

Whitespace-only changes.

0 commit comments

Comments
 (0)