Skip to content

Commit ae80b60

Browse files
committed
Add vue/script-setup-uses-vars rule
1 parent a770662 commit ae80b60

15 files changed

+565
-64
lines changed

Diff for: docs/.vuepress/components/eslint-code-block.vue

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export default {
135135
linter.defineRule(`vue/${ruleId}`, rules[ruleId])
136136
}
137137
linter.defineRule('no-undef', coreRules['no-undef'])
138+
linter.defineRule('no-unused-vars', coreRules['no-unused-vars'])
138139
139140
linter.defineParser('vue-eslint-parser', { parseForESLint })
140141
}

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
2525
|:--------|:------------|:---|
2626
| [vue/comment-directive](./comment-directive.md) | support comment-directives in `<template>` | |
2727
| [vue/jsx-uses-vars](./jsx-uses-vars.md) | prevent variables used in JSX to be marked as unused | |
28+
| [vue/script-setup-uses-vars](./script-setup-uses-vars.md) | prevent `<script setup>` variables used in `<template>` to be marked as unused | |
2829

2930
## Priority A: Essential (Error Prevention) <badge text="for Vue.js 3.x" vertical="middle">for Vue.js 3.x</badge>
3031

Diff for: docs/rules/jsx-uses-vars.md

+5
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ After turning on, `HelloWorld` is being marked as used and `no-unused-vars` rule
3838

3939
If you are not using JSX or if you do not use the `no-unused-vars` rule then you can disable this rule.
4040

41+
## :couple: Related Rules
42+
43+
- [vue/script-setup-uses-vars](./script-setup-uses-vars.md)
44+
- [no-unused-vars](https://eslint.org/docs/rules/no-unused-vars)
45+
4146
## :rocket: Version
4247

4348
This rule was introduced in eslint-plugin-vue v2.0.0

Diff for: docs/rules/script-setup-uses-vars.md

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/script-setup-uses-vars
5+
description: prevent `<script setup>` variables used in `<template>` to be marked as unused
6+
---
7+
# vue/script-setup-uses-vars
8+
9+
> prevent `<script setup>` variables used in `<template>` to be marked as unused
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :gear: This rule is included in all of `"plugin:vue/base"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-essential"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/recommended"` and `"plugin:vue/vue3-recommended"`.
13+
14+
ESLint `no-unused-vars` rule does not detect variables in `<script setup>` used in `<template>`.
15+
This rule will find variables in `<script setup>` used in `<template>` and mark them as used.
16+
17+
This rule only has an effect when the `no-unused-vars` rule is enabled.
18+
19+
## :book: Rule Details
20+
21+
Without this rule this code triggers warning:
22+
23+
<eslint-code-block :rules="{'vue/script-setup-uses-vars': ['error'], 'no-unused-vars': ['error']}">
24+
25+
```vue
26+
<script setup>
27+
// imported components are also directly usable in template
28+
import Foo from './Foo.vue'
29+
import { ref } from 'vue'
30+
31+
// write Composition API code just like in a normal setup()
32+
// but no need to manually return everything
33+
const count = ref(0)
34+
const inc = () => {
35+
count.value++
36+
}
37+
</script>
38+
39+
<template>
40+
<Foo :count="count" @click="inc" />
41+
</template>
42+
```
43+
44+
</eslint-code-block>
45+
46+
After turning on, `Foo` is being marked as used and `no-unused-vars` rule doesn't report an issue.
47+
48+
## :mute: When Not To Use It
49+
50+
If you are not using `<script setup>` or if you do not use the `no-unused-vars` rule then you can disable this rule.
51+
52+
## :couple: Related Rules
53+
54+
- [vue/jsx-uses-vars](./jsx-uses-vars.md)
55+
- [no-unused-vars](https://eslint.org/docs/rules/no-unused-vars)
56+
57+
## :mag: Implementation
58+
59+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/script-setup-uses-vars.js)
60+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/script-setup-uses-vars.js)

Diff for: lib/configs/base.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module.exports = {
1616
plugins: ['vue'],
1717
rules: {
1818
'vue/comment-directive': 'error',
19-
'vue/jsx-uses-vars': 'error'
19+
'vue/jsx-uses-vars': 'error',
20+
'vue/script-setup-uses-vars': 'error'
2021
}
2122
}

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ module.exports = {
155155
'return-in-computed-property': require('./rules/return-in-computed-property'),
156156
'return-in-emits-validator': require('./rules/return-in-emits-validator'),
157157
'script-indent': require('./rules/script-indent'),
158+
'script-setup-uses-vars': require('./rules/script-setup-uses-vars'),
158159
'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
159160
'sort-keys': require('./rules/sort-keys'),
160161
'space-in-parens': require('./rules/space-in-parens'),

Diff for: lib/rules/no-reserved-component-names.js

+7-20
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ const casing = require('../utils/casing')
1010
const htmlElements = require('../utils/html-elements.json')
1111
const deprecatedHtmlElements = require('../utils/deprecated-html-elements.json')
1212
const svgElements = require('../utils/svg-elements.json')
13+
const RESERVED_NAMES_IN_VUE = new Set(
14+
require('../utils/vue2-builtin-components')
15+
)
16+
17+
const RESERVED_NAMES_IN_VUE3 = new Set(
18+
require('../utils/vue3-builtin-components')
19+
)
1320

1421
const kebabCaseElements = [
1522
'annotation-xml',
@@ -22,17 +29,6 @@ const kebabCaseElements = [
2229
'missing-glyph'
2330
]
2431

25-
// https://vuejs.org/v2/api/index.html#Built-In-Components
26-
const vueBuiltInComponents = [
27-
'component',
28-
'transition',
29-
'transition-group',
30-
'keep-alive',
31-
'slot'
32-
]
33-
34-
const vue3BuiltInComponents = ['teleport', 'suspense']
35-
3632
/** @param {string} word */
3733
function isLowercase(word) {
3834
return /^[a-z]*$/.test(word)
@@ -42,15 +38,6 @@ const RESERVED_NAMES_IN_HTML = new Set([
4238
...htmlElements,
4339
...htmlElements.map(casing.capitalize)
4440
])
45-
const RESERVED_NAMES_IN_VUE = new Set([
46-
...vueBuiltInComponents,
47-
...vueBuiltInComponents.map(casing.pascalCase)
48-
])
49-
const RESERVED_NAMES_IN_VUE3 = new Set([
50-
...RESERVED_NAMES_IN_VUE,
51-
...vue3BuiltInComponents,
52-
...vue3BuiltInComponents.map(casing.pascalCase)
53-
])
5441
const RESERVED_NAMES_IN_OTHERS = new Set([
5542
...deprecatedHtmlElements,
5643
...deprecatedHtmlElements.map(casing.capitalize),

Diff for: lib/rules/no-unregistered-components.js

+1-10
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,6 @@ const casing = require('../utils/casing')
1515
// Rule helpers
1616
// ------------------------------------------------------------------------------
1717

18-
const VUE_BUILT_IN_COMPONENTS = [
19-
'component',
20-
'suspense',
21-
'teleport',
22-
'transition',
23-
'transition-group',
24-
'keep-alive',
25-
'slot'
26-
]
2718
/**
2819
* Check whether the given node is a built-in component or not.
2920
*
@@ -37,7 +28,7 @@ const isBuiltInComponent = (node) => {
3728
return (
3829
utils.isHtmlElementNode(node) &&
3930
!utils.isHtmlWellKnownElementName(node.rawName) &&
40-
VUE_BUILT_IN_COMPONENTS.indexOf(rawName) > -1
31+
utils.isBuiltInComponentName(rawName)
4132
)
4233
}
4334

Diff for: lib/rules/script-setup-uses-vars.js

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
const casing = require('../utils/casing')
13+
14+
// ------------------------------------------------------------------------------
15+
// Rule Definition
16+
// ------------------------------------------------------------------------------
17+
18+
module.exports = {
19+
meta: {
20+
type: 'problem',
21+
docs: {
22+
description:
23+
'prevent `<script setup>` variables used in `<template>` to be marked as unused', // eslint-disable-line consistent-docs-description
24+
categories: ['base'],
25+
url: 'https://eslint.vuejs.org/rules/script-setup-uses-vars.html'
26+
},
27+
schema: []
28+
},
29+
/**
30+
* @param {RuleContext} context - The rule context.
31+
* @returns {RuleListener} AST event handlers.
32+
*/
33+
create(context) {
34+
const df =
35+
context.parserServices.getDocumentFragment &&
36+
context.parserServices.getDocumentFragment()
37+
if (!df) {
38+
return {}
39+
}
40+
const scriptSetupNode = df.children
41+
.filter(utils.isVElement)
42+
.find((e) => e.name === 'script' && utils.hasAttribute(e, 'setup'))
43+
if (!scriptSetupNode) {
44+
return {}
45+
}
46+
/** @type {Set<string>} */
47+
const scriptVariableNames = new Set()
48+
const globalScope = context.getSourceCode().scopeManager.globalScope
49+
if (globalScope) {
50+
for (const variable of globalScope.variables) {
51+
scriptVariableNames.add(variable.name)
52+
}
53+
const moduleScope = globalScope.childScopes.find(
54+
(scope) => scope.type === 'module'
55+
)
56+
for (const variable of (moduleScope && moduleScope.variables) || []) {
57+
scriptVariableNames.add(variable.name)
58+
}
59+
}
60+
61+
/**
62+
* `casing.camelCase()` converts the beginning to lowercase,
63+
* but does not convert the case of the beginning character when converting with Vue3.
64+
* @see https://github.com/vuejs/vue-next/blob/1ffd48a2f5fd3eead3ea29dae668b7ed1c6f6130/packages/shared/src/index.ts#L116
65+
* @param {string} str
66+
*/
67+
function camelize(str) {
68+
return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
69+
}
70+
/**
71+
* @see https://github.com/vuejs/vue-next/blob/1ffd48a2f5fd3eead3ea29dae668b7ed1c6f6130/packages/compiler-core/src/transforms/transformElement.ts#L321
72+
* @param {string} name
73+
*/
74+
function markElementVariableAsUsed(name) {
75+
if (scriptVariableNames.has(name)) {
76+
context.markVariableAsUsed(name)
77+
}
78+
const camelName = camelize(name)
79+
if (scriptVariableNames.has(camelName)) {
80+
context.markVariableAsUsed(camelName)
81+
}
82+
const pascalName = casing.capitalize(camelName)
83+
if (scriptVariableNames.has(pascalName)) {
84+
context.markVariableAsUsed(pascalName)
85+
}
86+
}
87+
88+
return utils.defineTemplateBodyVisitor(
89+
context,
90+
{
91+
VExpressionContainer(node) {
92+
for (const ref of node.references.filter(
93+
(ref) => ref.variable == null
94+
)) {
95+
context.markVariableAsUsed(ref.id.name)
96+
}
97+
},
98+
VElement(node) {
99+
if (
100+
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
101+
(node.rawName === node.name &&
102+
(utils.isHtmlWellKnownElementName(node.rawName) ||
103+
utils.isSvgWellKnownElementName(node.rawName))) ||
104+
utils.isBuiltInComponentName(node.rawName)
105+
) {
106+
return
107+
}
108+
markElementVariableAsUsed(node.rawName)
109+
},
110+
/** @param {VDirective} node */
111+
'VAttribute[directive=true]'(node) {
112+
if (utils.isBuiltInDirectiveName(node.key.name.name)) {
113+
return
114+
}
115+
markElementVariableAsUsed(`v-${node.key.name.rawName}`)
116+
}
117+
},
118+
undefined,
119+
{
120+
templateBodyTriggerSelector: 'Program'
121+
}
122+
)
123+
}
124+
}

Diff for: lib/utils/index.js

+50-2
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@
9999
const HTML_ELEMENT_NAMES = new Set(require('./html-elements.json'))
100100
const SVG_ELEMENT_NAMES = new Set(require('./svg-elements.json'))
101101
const VOID_ELEMENT_NAMES = new Set(require('./void-elements.json'))
102+
const VUE2_BUILTIN_COMPONENT_NAMES = new Set(
103+
require('./vue2-builtin-components')
104+
)
105+
const VUE3_BUILTIN_COMPONENT_NAMES = new Set(
106+
require('./vue3-builtin-components')
107+
)
102108
const path = require('path')
103109
const vueEslintParser = require('vue-eslint-parser')
104110
const traverseNodes = vueEslintParser.AST.traverseNodes
@@ -277,6 +283,7 @@ module.exports = {
277283
* @param {RuleContext} context The rule context to use parser services.
278284
* @param {TemplateListener} templateBodyVisitor The visitor to traverse the template body.
279285
* @param {RuleListener} [scriptVisitor] The visitor to traverse the script.
286+
* @param { { templateBodyTriggerSelector: "Program" | "Program:exit" } } [options] The options.
280287
* @returns {RuleListener} The merged visitor.
281288
*/
282289
defineTemplateBodyVisitor,
@@ -737,6 +744,44 @@ module.exports = {
737744
isHtmlVoidElementName(name) {
738745
return VOID_ELEMENT_NAMES.has(name)
739746
},
747+
748+
/**
749+
* Check whether the given name is Vue builtin component name or not.
750+
* @param {string} name The name to check.
751+
* @returns {boolean} `true` if the name is a builtin component name
752+
*/
753+
isBuiltInComponentName(name) {
754+
return (
755+
VUE3_BUILTIN_COMPONENT_NAMES.has(name) ||
756+
VUE2_BUILTIN_COMPONENT_NAMES.has(name)
757+
)
758+
},
759+
760+
/**
761+
* Check whether the given name is Vue builtin directive name or not.
762+
* @param {string} name The name to check.
763+
* @returns {boolean} `true` if the name is a builtin Directive name
764+
*/
765+
isBuiltInDirectiveName(name) {
766+
return (
767+
name === 'bind' ||
768+
name === 'on' ||
769+
name === 'text' ||
770+
name === 'html' ||
771+
name === 'show' ||
772+
name === 'if' ||
773+
name === 'else' ||
774+
name === 'else-if' ||
775+
name === 'for' ||
776+
name === 'model' ||
777+
name === 'slot' ||
778+
name === 'pre' ||
779+
name === 'cloak' ||
780+
name === 'once' ||
781+
name === 'is'
782+
)
783+
},
784+
740785
/**
741786
* Gets the property name of a given node.
742787
* @param {Property|AssignmentProperty|MethodDefinition|MemberExpression} node - The node to get.
@@ -1668,12 +1713,14 @@ function isDef(v) {
16681713
* @param {RuleContext} context The rule context to use parser services.
16691714
* @param {TemplateListener} templateBodyVisitor The visitor to traverse the template body.
16701715
* @param {RuleListener} [scriptVisitor] The visitor to traverse the script.
1716+
* @param { { templateBodyTriggerSelector: "Program" | "Program:exit" } } [options] The options.
16711717
* @returns {RuleListener} The merged visitor.
16721718
*/
16731719
function defineTemplateBodyVisitor(
16741720
context,
16751721
templateBodyVisitor,
1676-
scriptVisitor
1722+
scriptVisitor,
1723+
options
16771724
) {
16781725
if (context.parserServices.defineTemplateBodyVisitor == null) {
16791726
const filename = context.getFilename()
@@ -1688,7 +1735,8 @@ function defineTemplateBodyVisitor(
16881735
}
16891736
return context.parserServices.defineTemplateBodyVisitor(
16901737
templateBodyVisitor,
1691-
scriptVisitor
1738+
scriptVisitor,
1739+
options
16921740
)
16931741
}
16941742

0 commit comments

Comments
 (0)