Skip to content

Commit 9a99fe2

Browse files
authored
Add vue/script-setup-uses-vars rule (#1529)
* Add `vue/script-setup-uses-vars` rule * upgrade parser * Update * update test
1 parent a770662 commit 9a99fe2

16 files changed

+742
-144
lines changed

docs/.vuepress/components/eslint-code-block.vue

Lines changed: 1 addition & 0 deletions
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
}

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
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

docs/rules/jsx-uses-vars.md

Lines changed: 5 additions & 0 deletions
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

docs/rules/script-setup-uses-vars.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
## :books: Further Reading
58+
59+
- [Vue RFCs - 0040-script-setup](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md)
60+
61+
## :mag: Implementation
62+
63+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/script-setup-uses-vars.js)
64+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/script-setup-uses-vars.js)

lib/configs/base.js

Lines changed: 2 additions & 1 deletion
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
}

lib/index.js

Lines changed: 1 addition & 0 deletions
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'),

lib/rules/no-reserved-component-names.js

Lines changed: 7 additions & 20 deletions
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),

lib/rules/no-unregistered-components.js

Lines changed: 1 addition & 10 deletions
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

lib/rules/script-setup-uses-vars.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
if (!utils.isScriptSetup(context)) {
35+
return {}
36+
}
37+
/** @type {Set<string>} */
38+
const scriptVariableNames = new Set()
39+
const globalScope = context.getSourceCode().scopeManager.globalScope
40+
if (globalScope) {
41+
for (const variable of globalScope.variables) {
42+
scriptVariableNames.add(variable.name)
43+
}
44+
const moduleScope = globalScope.childScopes.find(
45+
(scope) => scope.type === 'module'
46+
)
47+
for (const variable of (moduleScope && moduleScope.variables) || []) {
48+
scriptVariableNames.add(variable.name)
49+
}
50+
}
51+
52+
/**
53+
* `casing.camelCase()` converts the beginning to lowercase,
54+
* but does not convert the case of the beginning character when converting with Vue3.
55+
* @see https://github.com/vuejs/vue-next/blob/1ffd48a2f5fd3eead3ea29dae668b7ed1c6f6130/packages/shared/src/index.ts#L116
56+
* @param {string} str
57+
*/
58+
function camelize(str) {
59+
return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
60+
}
61+
/**
62+
* @see https://github.com/vuejs/vue-next/blob/1ffd48a2f5fd3eead3ea29dae668b7ed1c6f6130/packages/compiler-core/src/transforms/transformElement.ts#L321
63+
* @param {string} name
64+
*/
65+
function markElementVariableAsUsed(name) {
66+
if (scriptVariableNames.has(name)) {
67+
context.markVariableAsUsed(name)
68+
}
69+
const camelName = camelize(name)
70+
if (scriptVariableNames.has(camelName)) {
71+
context.markVariableAsUsed(camelName)
72+
}
73+
const pascalName = casing.capitalize(camelName)
74+
if (scriptVariableNames.has(pascalName)) {
75+
context.markVariableAsUsed(pascalName)
76+
}
77+
}
78+
79+
return utils.defineTemplateBodyVisitor(
80+
context,
81+
{
82+
VExpressionContainer(node) {
83+
for (const ref of node.references.filter(
84+
(ref) => ref.variable == null
85+
)) {
86+
context.markVariableAsUsed(ref.id.name)
87+
}
88+
},
89+
VElement(node) {
90+
if (
91+
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
92+
(node.rawName === node.name &&
93+
(utils.isHtmlWellKnownElementName(node.rawName) ||
94+
utils.isSvgWellKnownElementName(node.rawName))) ||
95+
utils.isBuiltInComponentName(node.rawName)
96+
) {
97+
return
98+
}
99+
markElementVariableAsUsed(node.rawName)
100+
},
101+
/** @param {VDirective} node */
102+
'VAttribute[directive=true]'(node) {
103+
if (utils.isBuiltInDirectiveName(node.key.name.name)) {
104+
return
105+
}
106+
markElementVariableAsUsed(`v-${node.key.name.rawName}`)
107+
}
108+
},
109+
undefined,
110+
{
111+
templateBodyTriggerSelector: 'Program'
112+
}
113+
)
114+
}
115+
}

0 commit comments

Comments
 (0)