Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vue/script-setup-uses-vars rule #1529

Merged
merged 4 commits into from Jul 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vuepress/components/eslint-code-block.vue
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
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
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
@@ -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
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
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
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
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
@@ -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'
}
)
}
}