Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
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 | | :warning: |
| [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
89 changes: 89 additions & 0 deletions docs/rules/no-undef-directives.md
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"]

Check failure on line 74 in docs/rules/no-undef-directives.md

View workflow job for this annotation

GitHub Actions / Lint

Label reference '"foo"' not found

<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)
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
256 changes: 256 additions & 0 deletions lib/rules/no-undef-directives.js
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) {
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)
}

class DefinedInSetupDirectives {
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 {
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',
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
}
],
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 || []
Copy link
Member

@FloEdelmann FloEdelmann Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, please rename the option to ignore and use the regexp.toRegExpGroupMatcher util. Then the option supports both literals ("foo" would only ignore v-foo but not v-foo-bar), and regexp patterns ("/foo/" would also ignore v-foo-bar).


/**
* 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 = {}
/** @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) || []) {
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 {
// 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
)
}
}
Loading
Loading