-
-
Notifications
You must be signed in to change notification settings - Fork 696
feat: add vue/no-undef-directives rule #2990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rzzf
wants to merge
20
commits into
vuejs:master
Choose a base branch
from
rzzf:feat/vue-no-undef-directives
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+823
−0
Open
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
5ab0de6
feat: add vue/no-undef-directives rule
rzzf f46ad20
docs: update
rzzf 93e2771
chore: cleanup and update tests
rzzf 7644d32
Update docs/rules/no-undef-directives.md
rzzf 30b3a9a
Update docs/rules/no-undef-directives.md
rzzf a5dad27
Update tests/lib/rules/no-undef-directives.js
rzzf 17c56ce
Update tests/lib/rules/no-undef-directives.js
rzzf 0a2720e
Update lib/rules/no-undef-directives.js
rzzf ead4546
Update lib/rules/no-undef-directives.js
rzzf b5fe004
Apply suggestion from @FloEdelmann
rzzf 5d4734d
refactor: apply suggest
rzzf bdbb46c
docs: update
rzzf f589ad7
docs: update
rzzf b3bef99
refactor: apply suggest
rzzf a68a425
Update lib/rules/no-undef-directives.js
rzzf 41e43a3
Update lib/rules/no-undef-directives.js
rzzf f821bbe
docs: update
rzzf 3d956c5
Add changeset
FloEdelmann 26e14ac
Increase version bump to minor
FloEdelmann bbc191e
refactor: update
rzzf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| --- | ||
| pageClass: rule-details | ||
| sidebarDepth: 0 | ||
| title: vue/no-undef-directives | ||
| description: disallow use of undefined custom directives | ||
| --- | ||
|
|
||
| # vue/no-undef-directives | ||
|
|
||
| > disallow use of undefined custom directives | ||
|
|
||
| - :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge> | ||
|
|
||
| ## :book: Rule Details | ||
|
|
||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| <eslint-code-block :rules="{'vue/no-undef-directives': ['error']}"> | ||
|
|
||
| ```vue | ||
| <script setup> | ||
| import vFocus from './vFocus'; | ||
| </script> | ||
|
|
||
| <template> | ||
| <!-- ✓ GOOD --> | ||
| <input v-focus> | ||
|
|
||
| <!-- ✗ BAD --> | ||
| <div v-foo></div> | ||
| </template> | ||
| ``` | ||
|
|
||
| </eslint-code-block> | ||
|
|
||
| <eslint-code-block :rules="{'vue/no-undef-directives': ['error']}"> | ||
|
|
||
| ```vue | ||
| <template> | ||
| <!-- ✓ GOOD --> | ||
| <input v-focus> | ||
|
|
||
| <!-- ✗ BAD --> | ||
| <div v-foo></div> | ||
| </template> | ||
|
|
||
| <script> | ||
| import vFocus from './vFocus'; | ||
|
|
||
| export default { | ||
| directives: { | ||
| focus: vFocus | ||
| } | ||
| } | ||
| </script> | ||
| ``` | ||
|
|
||
| </eslint-code-block> | ||
|
|
||
| ## :wrench: Options | ||
|
|
||
| ```json | ||
| { | ||
| "vue/no-undef-directives": ["error", { | ||
| "ignore": ["foo"] | ||
| }] | ||
| } | ||
| ``` | ||
|
|
||
| - `"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. | ||
|
|
||
| ### `"ignore": ["foo"]` | ||
|
|
||
| <eslint-code-block :rules="{'vue/no-undef-directives': ['error', {ignore: ['foo']}]}"> | ||
|
|
||
| ```vue | ||
| <template> | ||
| <!-- ✓ GOOD --> | ||
| <div v-foo></div> | ||
| </template> | ||
| ``` | ||
|
|
||
| </eslint-code-block> | ||
|
|
||
| ## :mag: Implementation | ||
|
|
||
| - [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-undef-directives.js) | ||
| - [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-undef-directives.js) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| /** | ||
| * @author rzzf | ||
| * See LICENSE file in root directory for full license. | ||
| */ | ||
| 'use strict' | ||
|
|
||
| const utils = require('../utils') | ||
| const casing = require('../utils/casing') | ||
| const regexp = require('../utils/regexp') | ||
|
|
||
| /** | ||
| * @param {ObjectExpression} componentObject | ||
| * @returns { { node: Property, name: string }[] } Array of ASTNodes | ||
| */ | ||
| function getRegisteredDirectives(componentObject) { | ||
| const directivesNode = componentObject.properties.find( | ||
| (p) => | ||
| p.type === 'Property' && | ||
| utils.getStaticPropertyName(p) === 'directives' && | ||
| p.value.type === 'ObjectExpression' | ||
| ) | ||
|
|
||
| if ( | ||
| !directivesNode || | ||
| directivesNode.type !== 'Property' || | ||
| directivesNode.value.type !== 'ObjectExpression' | ||
| ) { | ||
| return [] | ||
| } | ||
|
|
||
| // @ts-ignore | ||
| return directivesNode.value.properties.flatMap((node) => { | ||
| const name = | ||
| node.type === 'Property' ? utils.getStaticPropertyName(node) : null | ||
| return name ? [{ node, name }] : [] | ||
| }) | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} rawName | ||
| * @param {Set<string>} definedNames | ||
| */ | ||
| function isDefinedInSetup(rawName, definedNames) { | ||
| const camelName = casing.camelCase(rawName) | ||
| const variableName = `v${casing.capitalize(camelName)}` | ||
| return definedNames.has(variableName) | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} rawName | ||
| * @param {Set<string>} definedNames | ||
| */ | ||
| function isDefinedInOptions(rawName, definedNames) { | ||
| const camelName = casing.camelCase(rawName) | ||
|
|
||
| if (definedNames.has(rawName)) { | ||
| return true | ||
| } | ||
|
|
||
| // allow case-insensitive only when the directive name itself contains capitalized letters | ||
| for (const name of definedNames) { | ||
| if ( | ||
| name.toLowerCase() === camelName.toLowerCase() && | ||
| name !== name.toLowerCase() | ||
| ) { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| module.exports = { | ||
| meta: { | ||
| type: 'problem', | ||
rzzf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| docs: { | ||
| description: 'disallow use of undefined custom directives', | ||
| categories: undefined, | ||
| url: 'https://eslint.vuejs.org/rules/no-undef-directives.html' | ||
| }, | ||
| fixable: null, | ||
| schema: [ | ||
| { | ||
| type: 'object', | ||
| properties: { | ||
| ignore: { | ||
| type: 'array', | ||
| items: { type: 'string' }, | ||
| uniqueItems: true | ||
| } | ||
| }, | ||
| additionalProperties: true | ||
rzzf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| ], | ||
| messages: { | ||
| undef: "The 'v-{{name}}' directive has been used, but not defined." | ||
| } | ||
| }, | ||
| /** @param {RuleContext} context */ | ||
| create(context) { | ||
| const options = context.options[0] || {} | ||
| const { ignore = [] } = options | ||
| const isAnyIgnored = regexp.toRegExpGroupMatcher(ignore) | ||
|
|
||
| /** | ||
| * Check whether the given directive name is a verify target or not. | ||
| * | ||
| * @param {string} rawName The directive name. | ||
| * @returns {boolean} | ||
| */ | ||
| function isVerifyTargetDirective(rawName) { | ||
| const kebabName = casing.kebabCase(rawName) | ||
| if ( | ||
| utils.isBuiltInDirectiveName(rawName) || | ||
| isAnyIgnored(rawName, kebabName) | ||
| ) { | ||
| return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| /** | ||
| * @param {(rawName: string) => boolean} isDefined | ||
| * @returns {TemplateListener} | ||
| */ | ||
| function createTemplateBodyVisitor(isDefined) { | ||
| return { | ||
| /** @param {VDirective} node */ | ||
| 'VAttribute[directive=true]'(node) { | ||
| const name = node.key.name.name | ||
| if (utils.isBuiltInDirectiveName(name)) { | ||
| return | ||
| } | ||
| const rawName = node.key.name.rawName || name | ||
| if (isVerifyTargetDirective(rawName) && !isDefined(rawName)) { | ||
| context.report({ | ||
| node: node.key, | ||
| messageId: 'undef', | ||
| data: { | ||
| name: rawName | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** @type {Set<string>} */ | ||
| const definedInOptionDirectives = new Set() | ||
|
|
||
| if (utils.isScriptSetup(context)) { | ||
| // For <script setup> | ||
| /** @type {Set<string>} */ | ||
| const definedInSetupDirectives = new Set() | ||
|
|
||
| const globalScope = context.sourceCode.scopeManager.globalScope | ||
| if (globalScope) { | ||
| for (const variable of globalScope.variables) { | ||
| definedInSetupDirectives.add(variable.name) | ||
| } | ||
| const moduleScope = globalScope.childScopes.find( | ||
| (scope) => scope.type === 'module' | ||
| ) | ||
| for (const variable of moduleScope?.variables ?? []) { | ||
| definedInSetupDirectives.add(variable.name) | ||
| } | ||
| } | ||
|
|
||
| const scriptVisitor = utils.defineVueVisitor(context, { | ||
| onVueObjectEnter(node) { | ||
| for (const directive of getRegisteredDirectives(node)) { | ||
| definedInOptionDirectives.add(directive.name) | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| const templateBodyVisitor = createTemplateBodyVisitor( | ||
| (rawName) => | ||
| isDefinedInSetup(rawName, definedInSetupDirectives) || | ||
| isDefinedInOptions(rawName, definedInOptionDirectives) | ||
| ) | ||
|
|
||
| return utils.defineTemplateBodyVisitor( | ||
| context, | ||
| templateBodyVisitor, | ||
| scriptVisitor | ||
| ) | ||
| } | ||
|
|
||
| // For Options API | ||
| const scriptVisitor = utils.executeOnVue(context, (obj) => { | ||
| for (const directive of getRegisteredDirectives(obj)) { | ||
| definedInOptionDirectives.add(directive.name) | ||
| } | ||
| }) | ||
|
|
||
| const templateBodyVisitor = createTemplateBodyVisitor((rawName) => | ||
| isDefinedInOptions(rawName, definedInOptionDirectives) | ||
| ) | ||
|
|
||
| return utils.defineTemplateBodyVisitor( | ||
| context, | ||
| templateBodyVisitor, | ||
| scriptVisitor | ||
| ) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is a
@ts-ignorecomment needed?