-
-
Notifications
You must be signed in to change notification settings - Fork 697
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
19
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.
Open
Changes from 9 commits
Commits
Show all changes
19 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 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,89 @@ | ||
| --- | ||
| 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", { | ||
| "ignorePatterns": ["foo"] | ||
| }] | ||
| } | ||
| ``` | ||
|
|
||
| - `ignorePatterns` (`string[]`) ... An array of regex pattern strings to ignore. | ||
|
|
||
| ### "ignorePatterns": ["foo"] | ||
|
|
||
| <eslint-code-block :rules="{'vue/no-undef-directives': ['error', {ignorePatterns: ['foo']}]}"> | ||
|
|
||
| ```vue | ||
| <template> | ||
| <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,256 @@ | ||
| /** | ||
| * @author rzzf | ||
| * See LICENSE file in root directory for full license. | ||
| */ | ||
| 'use strict' | ||
|
|
||
| const utils = require('../utils') | ||
| const casing = require('../utils/casing') | ||
|
|
||
| /** | ||
| * @param {string} str | ||
| * @returns {string} | ||
| */ | ||
| function camelize(str) { | ||
rzzf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : '')) | ||
| } | ||
|
|
||
| /** | ||
| * @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 [] | ||
| } | ||
|
|
||
| return directivesNode.value.properties | ||
| .filter((node) => node.type === 'Property') | ||
| .map((node) => { | ||
| const name = utils.getStaticPropertyName(node) | ||
| return name ? { node, name } : null | ||
| }) | ||
| .filter((res) => !!res) | ||
rzzf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| class DefinedInSetupDirectives { | ||
rzzf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| constructor() { | ||
| /** | ||
| * Directive names | ||
| * @type {Set<string>} | ||
| */ | ||
| this.names = new Set() | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} name | ||
| */ | ||
| addName(name) { | ||
| this.names.add(name) | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} rawName | ||
| */ | ||
| isDefinedDirective(rawName) { | ||
| const camelName = camelize(rawName) | ||
| const variableName = `v${casing.capitalize(camelName)}` | ||
| return this.names.has(variableName) | ||
| } | ||
| } | ||
|
|
||
| class DefinedInOptionDirectives { | ||
rzzf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| constructor() { | ||
| /** | ||
| * Directive names | ||
| * @type {Set<string>} | ||
| */ | ||
| this.names = new Set() | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} name | ||
| */ | ||
| addName(name) { | ||
| this.names.add(name) | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} rawName | ||
| */ | ||
| isDefinedDirective(rawName) { | ||
| const camelName = camelize(rawName) | ||
| if (this.names.has(rawName) || this.names.has(camelName)) { | ||
| return true | ||
| } | ||
|
|
||
| // allow case-insensitive ONLY when the directive name itself contains capitalized letters | ||
| for (const name of this.names) { | ||
| 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: { | ||
| ignorePatterns: { | ||
| 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] || {} | ||
| /** @type {string[]} */ | ||
| const ignorePatterns = options.ignorePatterns || [] | ||
|
||
|
|
||
| /** | ||
| * Check whether the given directive name is a verify target or not. | ||
| * | ||
| * @param {string} rawName The directive name. | ||
| * @returns {boolean} | ||
| */ | ||
| function isVerifyTargetDirective(rawName) { | ||
| if (utils.isBuiltInDirectiveName(rawName)) { | ||
| return false | ||
| } | ||
|
|
||
| const ignored = ignorePatterns.some((pattern) => | ||
| new RegExp(pattern).test(rawName) | ||
| ) | ||
| return !ignored | ||
| } | ||
|
|
||
| /** @type { (rawName:string, reportNode: ASTNode) => void } */ | ||
| let verifyName | ||
| /** @type {RuleListener} */ | ||
| let scriptVisitor = {} | ||
rzzf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /** @type {TemplateListener} */ | ||
| const templateBodyVisitor = { | ||
| /** @param {VDirective} node */ | ||
| 'VAttribute[directive=true]'(node) { | ||
| const name = node.key.name.name | ||
| if (utils.isBuiltInDirectiveName(name)) { | ||
| return | ||
| } | ||
| verifyName(node.key.name.rawName || name, node.key) | ||
| } | ||
| } | ||
|
|
||
| if (utils.isScriptSetup(context)) { | ||
| // For <script setup> | ||
| const definedInSetupDirectives = new DefinedInSetupDirectives() | ||
| const definedInOptionDirectives = new DefinedInOptionDirectives() | ||
|
|
||
| const globalScope = context.sourceCode.scopeManager.globalScope | ||
| if (globalScope) { | ||
| for (const variable of globalScope.variables) { | ||
| definedInSetupDirectives.addName(variable.name) | ||
| } | ||
| const moduleScope = globalScope.childScopes.find( | ||
| (scope) => scope.type === 'module' | ||
| ) | ||
| for (const variable of (moduleScope && moduleScope.variables) || []) { | ||
rzzf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| definedInSetupDirectives.addName(variable.name) | ||
| } | ||
| } | ||
|
|
||
| scriptVisitor = utils.defineVueVisitor(context, { | ||
| onVueObjectEnter(node) { | ||
| for (const directive of getRegisteredDirectives(node)) { | ||
| definedInOptionDirectives.addName(directive.name) | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| verifyName = (rawName, reportNode) => { | ||
| if ( | ||
| !isVerifyTargetDirective(rawName) || | ||
| definedInSetupDirectives.isDefinedDirective(rawName) || | ||
| definedInOptionDirectives.isDefinedDirective(rawName) | ||
| ) { | ||
| return | ||
| } | ||
|
|
||
| context.report({ | ||
| node: reportNode, | ||
| messageId: 'undef', | ||
| data: { | ||
| name: rawName | ||
| } | ||
| }) | ||
| } | ||
| } else { | ||
rzzf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // For Options API | ||
| const definedInOptionDirectives = new DefinedInOptionDirectives() | ||
|
|
||
| scriptVisitor = utils.executeOnVue(context, (obj) => { | ||
| for (const directive of getRegisteredDirectives(obj)) { | ||
| definedInOptionDirectives.addName(directive.name) | ||
| } | ||
| }) | ||
|
|
||
| verifyName = (rawName, reportNode) => { | ||
| if ( | ||
| !isVerifyTargetDirective(rawName) || | ||
| definedInOptionDirectives.isDefinedDirective(rawName) | ||
| ) { | ||
| return | ||
| } | ||
|
|
||
| context.report({ | ||
| node: reportNode, | ||
| messageId: 'undef', | ||
| data: { | ||
| name: rawName | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| 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.
Uh oh!
There was an error while loading. Please reload this page.