Skip to content

Commit

Permalink
Add vue/script-setup-uses-vars rule (#1529)
Browse files Browse the repository at this point in the history
* Add `vue/script-setup-uses-vars` rule

* upgrade parser

* Update

* update test
  • Loading branch information
ota-meshi authored Jul 2, 2021
1 parent a770662 commit 9a99fe2
Show file tree
Hide file tree
Showing 16 changed files with 742 additions and 144 deletions.
1 change: 1 addition & 0 deletions docs/.vuepress/components/eslint-code-block.vue
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export default {
linter.defineRule(`vue/${ruleId}`, rules[ruleId])
}
linter.defineRule('no-undef', coreRules['no-undef'])
linter.defineRule('no-unused-vars', coreRules['no-unused-vars'])
linter.defineParser('vue-eslint-parser', { parseForESLint })
}
Expand Down
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
|:--------|:------------|:---|
| [vue/comment-directive](./comment-directive.md) | support comment-directives in `<template>` | |
| [vue/jsx-uses-vars](./jsx-uses-vars.md) | prevent variables used in JSX to be marked as unused | |
| [vue/script-setup-uses-vars](./script-setup-uses-vars.md) | prevent `<script setup>` variables used in `<template>` to be marked as unused | |

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

Expand Down
5 changes: 5 additions & 0 deletions docs/rules/jsx-uses-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ After turning on, `HelloWorld` is being marked as used and `no-unused-vars` rule

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

## :couple: Related Rules

- [vue/script-setup-uses-vars](./script-setup-uses-vars.md)
- [no-unused-vars](https://eslint.org/docs/rules/no-unused-vars)

## :rocket: Version

This rule was introduced in eslint-plugin-vue v2.0.0
Expand Down
64 changes: 64 additions & 0 deletions docs/rules/script-setup-uses-vars.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/script-setup-uses-vars
description: prevent `<script setup>` variables used in `<template>` to be marked as unused
---
# vue/script-setup-uses-vars

> prevent `<script setup>` variables used in `<template>` to be marked as unused
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :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"`.

ESLint `no-unused-vars` rule does not detect variables in `<script setup>` used in `<template>`.
This rule will find variables in `<script setup>` used in `<template>` and mark them as used.

This rule only has an effect when the `no-unused-vars` rule is enabled.

## :book: Rule Details

Without this rule this code triggers warning:

<eslint-code-block :rules="{'vue/script-setup-uses-vars': ['error'], 'no-unused-vars': ['error']}">

```vue
<script setup>
// imported components are also directly usable in template
import Foo from './Foo.vue'
import { ref } from 'vue'
// write Composition API code just like in a normal setup()
// but no need to manually return everything
const count = ref(0)
const inc = () => {
count.value++
}
</script>
<template>
<Foo :count="count" @click="inc" />
</template>
```

</eslint-code-block>

After turning on, `Foo` is being marked as used and `no-unused-vars` rule doesn't report an issue.

## :mute: When Not To Use It

If you are not using `<script setup>` or if you do not use the `no-unused-vars` rule then you can disable this rule.

## :couple: Related Rules

- [vue/jsx-uses-vars](./jsx-uses-vars.md)
- [no-unused-vars](https://eslint.org/docs/rules/no-unused-vars)

## :books: Further Reading

- [Vue RFCs - 0040-script-setup](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md)

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/script-setup-uses-vars.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/script-setup-uses-vars.js)
3 changes: 2 additions & 1 deletion lib/configs/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports = {
plugins: ['vue'],
rules: {
'vue/comment-directive': 'error',
'vue/jsx-uses-vars': 'error'
'vue/jsx-uses-vars': 'error',
'vue/script-setup-uses-vars': 'error'
}
}
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ module.exports = {
'return-in-computed-property': require('./rules/return-in-computed-property'),
'return-in-emits-validator': require('./rules/return-in-emits-validator'),
'script-indent': require('./rules/script-indent'),
'script-setup-uses-vars': require('./rules/script-setup-uses-vars'),
'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
'sort-keys': require('./rules/sort-keys'),
'space-in-parens': require('./rules/space-in-parens'),
Expand Down
27 changes: 7 additions & 20 deletions lib/rules/no-reserved-component-names.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ const casing = require('../utils/casing')
const htmlElements = require('../utils/html-elements.json')
const deprecatedHtmlElements = require('../utils/deprecated-html-elements.json')
const svgElements = require('../utils/svg-elements.json')
const RESERVED_NAMES_IN_VUE = new Set(
require('../utils/vue2-builtin-components')
)

const RESERVED_NAMES_IN_VUE3 = new Set(
require('../utils/vue3-builtin-components')
)

const kebabCaseElements = [
'annotation-xml',
Expand All @@ -22,17 +29,6 @@ const kebabCaseElements = [
'missing-glyph'
]

// https://vuejs.org/v2/api/index.html#Built-In-Components
const vueBuiltInComponents = [
'component',
'transition',
'transition-group',
'keep-alive',
'slot'
]

const vue3BuiltInComponents = ['teleport', 'suspense']

/** @param {string} word */
function isLowercase(word) {
return /^[a-z]*$/.test(word)
Expand All @@ -42,15 +38,6 @@ const RESERVED_NAMES_IN_HTML = new Set([
...htmlElements,
...htmlElements.map(casing.capitalize)
])
const RESERVED_NAMES_IN_VUE = new Set([
...vueBuiltInComponents,
...vueBuiltInComponents.map(casing.pascalCase)
])
const RESERVED_NAMES_IN_VUE3 = new Set([
...RESERVED_NAMES_IN_VUE,
...vue3BuiltInComponents,
...vue3BuiltInComponents.map(casing.pascalCase)
])
const RESERVED_NAMES_IN_OTHERS = new Set([
...deprecatedHtmlElements,
...deprecatedHtmlElements.map(casing.capitalize),
Expand Down
11 changes: 1 addition & 10 deletions lib/rules/no-unregistered-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,6 @@ const casing = require('../utils/casing')
// Rule helpers
// ------------------------------------------------------------------------------

const VUE_BUILT_IN_COMPONENTS = [
'component',
'suspense',
'teleport',
'transition',
'transition-group',
'keep-alive',
'slot'
]
/**
* Check whether the given node is a built-in component or not.
*
Expand All @@ -37,7 +28,7 @@ const isBuiltInComponent = (node) => {
return (
utils.isHtmlElementNode(node) &&
!utils.isHtmlWellKnownElementName(node.rawName) &&
VUE_BUILT_IN_COMPONENTS.indexOf(rawName) > -1
utils.isBuiltInComponentName(rawName)
)
}

Expand Down
115 changes: 115 additions & 0 deletions lib/rules/script-setup-uses-vars.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('../utils')
const casing = require('../utils/casing')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
type: 'problem',
docs: {
description:
'prevent `<script setup>` variables used in `<template>` to be marked as unused', // eslint-disable-line consistent-docs-description
categories: ['base'],
url: 'https://eslint.vuejs.org/rules/script-setup-uses-vars.html'
},
schema: []
},
/**
* @param {RuleContext} context - The rule context.
* @returns {RuleListener} AST event handlers.
*/
create(context) {
if (!utils.isScriptSetup(context)) {
return {}
}
/** @type {Set<string>} */
const scriptVariableNames = new Set()
const globalScope = context.getSourceCode().scopeManager.globalScope
if (globalScope) {
for (const variable of globalScope.variables) {
scriptVariableNames.add(variable.name)
}
const moduleScope = globalScope.childScopes.find(
(scope) => scope.type === 'module'
)
for (const variable of (moduleScope && moduleScope.variables) || []) {
scriptVariableNames.add(variable.name)
}
}

/**
* `casing.camelCase()` converts the beginning to lowercase,
* but does not convert the case of the beginning character when converting with Vue3.
* @see https://github.com/vuejs/vue-next/blob/1ffd48a2f5fd3eead3ea29dae668b7ed1c6f6130/packages/shared/src/index.ts#L116
* @param {string} str
*/
function camelize(str) {
return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
}
/**
* @see https://github.com/vuejs/vue-next/blob/1ffd48a2f5fd3eead3ea29dae668b7ed1c6f6130/packages/compiler-core/src/transforms/transformElement.ts#L321
* @param {string} name
*/
function markElementVariableAsUsed(name) {
if (scriptVariableNames.has(name)) {
context.markVariableAsUsed(name)
}
const camelName = camelize(name)
if (scriptVariableNames.has(camelName)) {
context.markVariableAsUsed(camelName)
}
const pascalName = casing.capitalize(camelName)
if (scriptVariableNames.has(pascalName)) {
context.markVariableAsUsed(pascalName)
}
}

return utils.defineTemplateBodyVisitor(
context,
{
VExpressionContainer(node) {
for (const ref of node.references.filter(
(ref) => ref.variable == null
)) {
context.markVariableAsUsed(ref.id.name)
}
},
VElement(node) {
if (
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
(node.rawName === node.name &&
(utils.isHtmlWellKnownElementName(node.rawName) ||
utils.isSvgWellKnownElementName(node.rawName))) ||
utils.isBuiltInComponentName(node.rawName)
) {
return
}
markElementVariableAsUsed(node.rawName)
},
/** @param {VDirective} node */
'VAttribute[directive=true]'(node) {
if (utils.isBuiltInDirectiveName(node.key.name.name)) {
return
}
markElementVariableAsUsed(`v-${node.key.name.rawName}`)
}
},
undefined,
{
templateBodyTriggerSelector: 'Program'
}
)
}
}
Loading

0 comments on commit 9a99fe2

Please sign in to comment.