Skip to content

Commit bcca364

Browse files
authored
Add vue/experimental-script-setup-vars rule (#1303)
* Add `vue/experimental-script-setup-vars` rule * update
1 parent 47ade60 commit bcca364

File tree

9 files changed

+393
-1
lines changed

9 files changed

+393
-1
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export default {
6262
config() {
6363
return {
6464
globals: {
65+
console: false,
6566
// ES2015 globals
6667
ArrayBuffer: false,
6768
DataView: false,
@@ -121,8 +122,13 @@ export default {
121122
122123
async mounted() {
123124
// Load linter.
124-
const [{ default: Linter }, { parseForESLint }] = await Promise.all([
125+
const [
126+
{ default: Linter },
127+
{ default: noUndefRule },
128+
{ parseForESLint }
129+
] = await Promise.all([
125130
import('eslint4b/dist/linter'),
131+
import('eslint/lib/rules/no-undef'),
126132
import('espree').then(() => import('vue-eslint-parser'))
127133
])
128134
@@ -131,6 +137,7 @@ export default {
131137
for (const ruleId of Object.keys(rules)) {
132138
linter.defineRule(`vue/${ruleId}`, rules[ruleId])
133139
}
140+
linter.defineRule('no-undef', noUndefRule)
134141
135142
linter.defineParser('vue-eslint-parser', { parseForESLint })
136143
}

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
2424
| Rule ID | Description | |
2525
|:--------|:------------|:---|
2626
| [vue/comment-directive](./comment-directive.md) | support comment-directives in `<template>` | |
27+
| [vue/experimental-script-setup-vars](./experimental-script-setup-vars.md) | prevent variables defined in `<script setup>` to be marked as undefined | |
2728
| [vue/jsx-uses-vars](./jsx-uses-vars.md) | prevent variables used in JSX 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>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/experimental-script-setup-vars
5+
description: prevent variables defined in `<script setup>` to be marked as undefined
6+
---
7+
# vue/experimental-script-setup-vars
8+
> prevent variables defined in `<script setup>` to be marked as undefined
9+
10+
- :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"`.
11+
12+
:::warning
13+
This rule is an experimental rule. It may be removed without notice.
14+
:::
15+
16+
This rule will find variables defined in `<script setup="args">` and mark them as defined variables.
17+
18+
This rule only has an effect when the `no-undef` rule is enabled.
19+
20+
## :book: Rule Details
21+
22+
Without this rule this code triggers warning:
23+
24+
<eslint-code-block :rules="{'no-undef': ['error'], 'vue/experimental-script-setup-vars': ['error']}">
25+
26+
```vue
27+
<script setup="props, { emit }">
28+
import { watchEffect } from 'vue'
29+
30+
watchEffect(() => console.log(props.msg))
31+
emit('foo')
32+
</script>
33+
```
34+
35+
</eslint-code-block>
36+
37+
After turning on, `props` and `emit` are being marked as defined and `no-undef` rule doesn't report an issue.
38+
39+
## :mag: Implementation
40+
41+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/experimental-script-setup-vars.js)
42+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/experimental-script-setup-vars.js)

lib/configs/base.js

Lines changed: 1 addition & 0 deletions
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/experimental-script-setup-vars': 'error',
1920
'vue/jsx-uses-vars': 'error'
2021
}
2122
}

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module.exports = {
2525
'dot-location': require('./rules/dot-location'),
2626
'dot-notation': require('./rules/dot-notation'),
2727
eqeqeq: require('./rules/eqeqeq'),
28+
'experimental-script-setup-vars': require('./rules/experimental-script-setup-vars'),
2829
'func-call-spacing': require('./rules/func-call-spacing'),
2930
'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
3031
'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/**
2+
* @fileoverview prevent variables defined in `<script setup>` to be marked as undefined
3+
* @author Yosuke Ota
4+
*/
5+
'use strict'
6+
7+
const Module = require('module')
8+
const path = require('path')
9+
const utils = require('../utils')
10+
const AST = require('vue-eslint-parser').AST
11+
12+
const ecmaVersion = 2020
13+
14+
// ------------------------------------------------------------------------------
15+
// Rule Definition
16+
// ------------------------------------------------------------------------------
17+
18+
module.exports = {
19+
meta: {
20+
type: 'problem',
21+
docs: {
22+
description:
23+
'prevent variables defined in `<script setup>` to be marked as undefined', // eslint-disable-line consistent-docs-description
24+
categories: ['base'],
25+
url: 'https://eslint.vuejs.org/rules/experimental-script-setup-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 documentFragment =
35+
context.parserServices.getDocumentFragment &&
36+
context.parserServices.getDocumentFragment()
37+
if (!documentFragment) {
38+
return {}
39+
}
40+
const sourceCode = context.getSourceCode()
41+
const scriptElement = documentFragment.children
42+
.filter(utils.isVElement)
43+
.find(
44+
(element) =>
45+
element.name === 'script' &&
46+
element.range[0] <= sourceCode.ast.range[0] &&
47+
sourceCode.ast.range[1] <= element.range[1]
48+
)
49+
if (!scriptElement) {
50+
return {}
51+
}
52+
const setupAttr = utils.getAttribute(scriptElement, 'setup')
53+
if (!setupAttr || !setupAttr.value) {
54+
return {}
55+
}
56+
const value = setupAttr.value.value
57+
58+
let eslintScope
59+
try {
60+
eslintScope = getESLintModule('eslint-scope', () =>
61+
// @ts-ignore
62+
require('eslint-scope')
63+
)
64+
} catch (_e) {
65+
context.report({
66+
node: setupAttr,
67+
message: 'Can not be resolved eslint-scope.'
68+
})
69+
return {}
70+
}
71+
let espree
72+
try {
73+
espree = getESLintModule('espree', () =>
74+
// @ts-ignore
75+
require('espree')
76+
)
77+
} catch (_e) {
78+
context.report({
79+
node: setupAttr,
80+
message: 'Can not be resolved espree.'
81+
})
82+
return {}
83+
}
84+
85+
const globalScope = sourceCode.scopeManager.scopes[0]
86+
87+
/** @type {string[]} */
88+
let vars
89+
try {
90+
vars = parseSetup(value, espree, eslintScope)
91+
} catch (_e) {
92+
context.report({
93+
node: setupAttr.value,
94+
message: 'Parsing error.'
95+
})
96+
return {}
97+
}
98+
99+
// Define configured global variables.
100+
for (const id of vars) {
101+
const tempVariable = globalScope.set.get(id)
102+
103+
/** @type {Variable} */
104+
let variable
105+
if (!tempVariable) {
106+
variable = new eslintScope.Variable(id, globalScope)
107+
108+
globalScope.variables.push(variable)
109+
globalScope.set.set(id, variable)
110+
} else {
111+
variable = tempVariable
112+
}
113+
114+
variable.eslintImplicitGlobalSetting = 'readonly'
115+
variable.eslintExplicitGlobal = undefined
116+
variable.eslintExplicitGlobalComments = undefined
117+
variable.writeable = false
118+
}
119+
120+
/*
121+
* "through" contains all references which definitions cannot be found.
122+
* Since we augment the global scope using configuration, we need to update
123+
* references and remove the ones that were added by configuration.
124+
*/
125+
globalScope.through = globalScope.through.filter((reference) => {
126+
const name = reference.identifier.name
127+
const variable = globalScope.set.get(name)
128+
129+
if (variable) {
130+
/*
131+
* Links the variable and the reference.
132+
* And this reference is removed from `Scope#through`.
133+
*/
134+
reference.resolved = variable
135+
variable.references.push(reference)
136+
137+
return false
138+
}
139+
140+
return true
141+
})
142+
143+
return {}
144+
}
145+
}
146+
147+
/**
148+
* @param {string} code
149+
* @param {any} espree
150+
* @param {any} eslintScope
151+
* @returns {string[]}
152+
*/
153+
function parseSetup(code, espree, eslintScope) {
154+
/** @type {Program} */
155+
const ast = espree.parse(`(${code})=>{}`, { ecmaVersion })
156+
const result = eslintScope.analyze(ast, {
157+
ignoreEval: true,
158+
nodejsScope: false,
159+
ecmaVersion,
160+
sourceType: 'script',
161+
fallback: AST.getFallbackKeys
162+
})
163+
164+
const variables = /** @type {Variable[]} */ (result.globalScope.childScopes[0]
165+
.variables)
166+
167+
return variables.map((v) => v.name)
168+
}
169+
170+
const createRequire =
171+
// Added in v12.2.0
172+
Module.createRequire ||
173+
// Added in v10.12.0, but deprecated in v12.2.0.
174+
Module.createRequireFromPath ||
175+
// Polyfill - This is not executed on the tests on node@>=10.
176+
/**
177+
* @param {string} filename
178+
*/
179+
function (filename) {
180+
const mod = new Module(filename)
181+
182+
mod.filename = filename
183+
// @ts-ignore
184+
mod.paths = Module._nodeModulePaths(path.dirname(filename))
185+
// @ts-ignore
186+
mod._compile('module.exports = require;', filename)
187+
return mod.exports
188+
}
189+
190+
/** @type { { 'espree'?: any, 'eslint-scope'?: any } } */
191+
const modulesCache = {}
192+
193+
/**
194+
* @param {string} p
195+
*/
196+
function isLinterPath(p) {
197+
return (
198+
// ESLint 6 and above
199+
p.includes(`eslint${path.sep}lib${path.sep}linter${path.sep}linter.js`) ||
200+
// ESLint 5
201+
p.includes(`eslint${path.sep}lib${path.sep}linter.js`)
202+
)
203+
}
204+
205+
/**
206+
* Load module from the loaded ESLint.
207+
* If the loaded ESLint was not found, just returns `fallback()`.
208+
* @param {'espree' | 'eslint-scope'} name
209+
* @param { () => any } fallback
210+
*/
211+
function getESLintModule(name, fallback) {
212+
if (!modulesCache[name]) {
213+
// Lookup the loaded eslint
214+
const linterPath = Object.keys(require.cache).find(isLinterPath)
215+
if (linterPath) {
216+
try {
217+
modulesCache[name] = createRequire(linterPath)(name)
218+
} catch (_e) {
219+
// ignore
220+
}
221+
}
222+
if (!modulesCache[name]) {
223+
modulesCache[name] = fallback()
224+
}
225+
}
226+
227+
return modulesCache[name]
228+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @author Yosuke Ota
3+
*/
4+
'use strict'
5+
6+
const { RuleTester } = require('eslint')
7+
const rule = require('../../../lib/rules/experimental-script-setup-vars')
8+
9+
const tester = new RuleTester({
10+
parser: require.resolve('vue-eslint-parser'),
11+
parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
12+
})
13+
14+
tester.run('experimental-script-setup-vars', rule, {
15+
valid: [
16+
`
17+
<script setup="props, { emit }">
18+
import { watchEffect } from 'vue'
19+
20+
watchEffect(() => console.log(props.msg))
21+
emit('foo')
22+
</script>`,
23+
`
24+
<script setup>
25+
export let count = 1
26+
</script>`,
27+
`
28+
<script>
29+
import { watchEffect } from 'vue'
30+
31+
export default {
32+
setup (props, { emit }) {
33+
watchEffect(() => console.log(props.msg))
34+
emit('foo')
35+
return {}
36+
}
37+
}
38+
</script>`,
39+
`
40+
<template>
41+
<div />
42+
</template>`
43+
],
44+
invalid: [
45+
{
46+
code: `
47+
<script setup="a - b">
48+
</script>
49+
`,
50+
errors: [
51+
{
52+
message: 'Parsing error.',
53+
line: 2
54+
}
55+
]
56+
}
57+
]
58+
})

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy