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
Open
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
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: '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: {
ignore: {
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] || {}
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