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)