Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 run-time 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`

<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
260 changes: 260 additions & 0 deletions lib/rules/no-undef-directives.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
/**
* @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[]} names
*/
addName(...names) {
for (const name of names) {
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[]} names
*/
addName(...names) {
for (const name of names) {
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 || []

/**
* 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