🌐 AI搜索 & 代理 主页
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fast-monkeys-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-vue": minor
---

Added new `vue/no-undef-directives` rule
2 changes: 2 additions & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ For example:
| [vue/no-template-target-blank] | disallow target="_blank" attribute without rel="noopener noreferrer" | :bulb: | :warning: |
| [vue/no-this-in-before-route-enter] | disallow `this` usage in a `beforeRouteEnter` method | | :warning: |
| [vue/no-undef-components] | disallow use of undefined components in `<template>` | | :hammer: |
| [vue/no-undef-directives] | disallow use of undefined custom directives | | :hammer: |
| [vue/no-undef-properties] | disallow undefined properties | | :hammer: |
| [vue/no-unsupported-features] | disallow unsupported Vue.js syntax on the specified version | :wrench: | :hammer: |
| [vue/no-unused-emit-declarations] | disallow unused emit declarations | | :hammer: |
Expand Down Expand Up @@ -521,6 +522,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
[vue/no-textarea-mustache]: ./no-textarea-mustache.md
[vue/no-this-in-before-route-enter]: ./no-this-in-before-route-enter.md
[vue/no-undef-components]: ./no-undef-components.md
[vue/no-undef-directives]: ./no-undef-directives.md
[vue/no-undef-properties]: ./no-undef-properties.md
[vue/no-unsupported-features]: ./no-unsupported-features.md
[vue/no-unused-components]: ./no-unused-components.md
Expand Down
90 changes: 90 additions & 0 deletions docs/rules/no-undef-directives.md
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)
1 change: 1 addition & 0 deletions lib/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ const plugin = {
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
'no-this-in-before-route-enter': require('./rules/no-this-in-before-route-enter'),
'no-undef-components': require('./rules/no-undef-components'),
'no-undef-directives': require('./rules/no-undef-directives'),
'no-undef-properties': require('./rules/no-undef-properties'),
'no-unsupported-features': require('./rules/no-unsupported-features'),
'no-unused-components': require('./rules/no-unused-components'),
Expand Down
207 changes: 207 additions & 0 deletions lib/rules/no-undef-directives.js
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: 'suggestion',
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: false
}
],
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
)
}
}
Loading
Loading