Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
vue/experimental-script-setup-vars
rule (#1303)
* Add `vue/experimental-script-setup-vars` rule * update
- Loading branch information
Showing
9 changed files
with
393 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
--- | ||
pageClass: rule-details | ||
sidebarDepth: 0 | ||
title: vue/experimental-script-setup-vars | ||
description: prevent variables defined in `<script setup>` to be marked as undefined | ||
--- | ||
# vue/experimental-script-setup-vars | ||
> prevent variables defined in `<script setup>` to be marked as undefined | ||
- :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"`. | ||
|
||
:::warning | ||
This rule is an experimental rule. It may be removed without notice. | ||
::: | ||
|
||
This rule will find variables defined in `<script setup="args">` and mark them as defined variables. | ||
|
||
This rule only has an effect when the `no-undef` rule is enabled. | ||
|
||
## :book: Rule Details | ||
|
||
Without this rule this code triggers warning: | ||
|
||
<eslint-code-block :rules="{'no-undef': ['error'], 'vue/experimental-script-setup-vars': ['error']}"> | ||
|
||
```vue | ||
<script setup="props, { emit }"> | ||
import { watchEffect } from 'vue' | ||
watchEffect(() => console.log(props.msg)) | ||
emit('foo') | ||
</script> | ||
``` | ||
|
||
</eslint-code-block> | ||
|
||
After turning on, `props` and `emit` are being marked as defined and `no-undef` rule doesn't report an issue. | ||
|
||
## :mag: Implementation | ||
|
||
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/experimental-script-setup-vars.js) | ||
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/experimental-script-setup-vars.js) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
/** | ||
* @fileoverview prevent variables defined in `<script setup>` to be marked as undefined | ||
* @author Yosuke Ota | ||
*/ | ||
'use strict' | ||
|
||
const Module = require('module') | ||
const path = require('path') | ||
const utils = require('../utils') | ||
const AST = require('vue-eslint-parser').AST | ||
|
||
const ecmaVersion = 2020 | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
|
||
module.exports = { | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: | ||
'prevent variables defined in `<script setup>` to be marked as undefined', // eslint-disable-line consistent-docs-description | ||
categories: ['base'], | ||
url: 'https://eslint.vuejs.org/rules/experimental-script-setup-vars.html' | ||
}, | ||
schema: [] | ||
}, | ||
/** | ||
* @param {RuleContext} context - The rule context. | ||
* @returns {RuleListener} AST event handlers. | ||
*/ | ||
create(context) { | ||
const documentFragment = | ||
context.parserServices.getDocumentFragment && | ||
context.parserServices.getDocumentFragment() | ||
if (!documentFragment) { | ||
return {} | ||
} | ||
const sourceCode = context.getSourceCode() | ||
const scriptElement = documentFragment.children | ||
.filter(utils.isVElement) | ||
.find( | ||
(element) => | ||
element.name === 'script' && | ||
element.range[0] <= sourceCode.ast.range[0] && | ||
sourceCode.ast.range[1] <= element.range[1] | ||
) | ||
if (!scriptElement) { | ||
return {} | ||
} | ||
const setupAttr = utils.getAttribute(scriptElement, 'setup') | ||
if (!setupAttr || !setupAttr.value) { | ||
return {} | ||
} | ||
const value = setupAttr.value.value | ||
|
||
let eslintScope | ||
try { | ||
eslintScope = getESLintModule('eslint-scope', () => | ||
// @ts-ignore | ||
require('eslint-scope') | ||
) | ||
} catch (_e) { | ||
context.report({ | ||
node: setupAttr, | ||
message: 'Can not be resolved eslint-scope.' | ||
}) | ||
return {} | ||
} | ||
let espree | ||
try { | ||
espree = getESLintModule('espree', () => | ||
// @ts-ignore | ||
require('espree') | ||
) | ||
} catch (_e) { | ||
context.report({ | ||
node: setupAttr, | ||
message: 'Can not be resolved espree.' | ||
}) | ||
return {} | ||
} | ||
|
||
const globalScope = sourceCode.scopeManager.scopes[0] | ||
|
||
/** @type {string[]} */ | ||
let vars | ||
try { | ||
vars = parseSetup(value, espree, eslintScope) | ||
} catch (_e) { | ||
context.report({ | ||
node: setupAttr.value, | ||
message: 'Parsing error.' | ||
}) | ||
return {} | ||
} | ||
|
||
// Define configured global variables. | ||
for (const id of vars) { | ||
const tempVariable = globalScope.set.get(id) | ||
|
||
/** @type {Variable} */ | ||
let variable | ||
if (!tempVariable) { | ||
variable = new eslintScope.Variable(id, globalScope) | ||
|
||
globalScope.variables.push(variable) | ||
globalScope.set.set(id, variable) | ||
} else { | ||
variable = tempVariable | ||
} | ||
|
||
variable.eslintImplicitGlobalSetting = 'readonly' | ||
variable.eslintExplicitGlobal = undefined | ||
variable.eslintExplicitGlobalComments = undefined | ||
variable.writeable = false | ||
} | ||
|
||
/* | ||
* "through" contains all references which definitions cannot be found. | ||
* Since we augment the global scope using configuration, we need to update | ||
* references and remove the ones that were added by configuration. | ||
*/ | ||
globalScope.through = globalScope.through.filter((reference) => { | ||
const name = reference.identifier.name | ||
const variable = globalScope.set.get(name) | ||
|
||
if (variable) { | ||
/* | ||
* Links the variable and the reference. | ||
* And this reference is removed from `Scope#through`. | ||
*/ | ||
reference.resolved = variable | ||
variable.references.push(reference) | ||
|
||
return false | ||
} | ||
|
||
return true | ||
}) | ||
|
||
return {} | ||
} | ||
} | ||
|
||
/** | ||
* @param {string} code | ||
* @param {any} espree | ||
* @param {any} eslintScope | ||
* @returns {string[]} | ||
*/ | ||
function parseSetup(code, espree, eslintScope) { | ||
/** @type {Program} */ | ||
const ast = espree.parse(`(${code})=>{}`, { ecmaVersion }) | ||
const result = eslintScope.analyze(ast, { | ||
ignoreEval: true, | ||
nodejsScope: false, | ||
ecmaVersion, | ||
sourceType: 'script', | ||
fallback: AST.getFallbackKeys | ||
}) | ||
|
||
const variables = /** @type {Variable[]} */ (result.globalScope.childScopes[0] | ||
.variables) | ||
|
||
return variables.map((v) => v.name) | ||
} | ||
|
||
const createRequire = | ||
// Added in v12.2.0 | ||
Module.createRequire || | ||
// Added in v10.12.0, but deprecated in v12.2.0. | ||
Module.createRequireFromPath || | ||
// Polyfill - This is not executed on the tests on node@>=10. | ||
/** | ||
* @param {string} filename | ||
*/ | ||
function (filename) { | ||
const mod = new Module(filename) | ||
|
||
mod.filename = filename | ||
// @ts-ignore | ||
mod.paths = Module._nodeModulePaths(path.dirname(filename)) | ||
// @ts-ignore | ||
mod._compile('module.exports = require;', filename) | ||
return mod.exports | ||
} | ||
|
||
/** @type { { 'espree'?: any, 'eslint-scope'?: any } } */ | ||
const modulesCache = {} | ||
|
||
/** | ||
* @param {string} p | ||
*/ | ||
function isLinterPath(p) { | ||
return ( | ||
// ESLint 6 and above | ||
p.includes(`eslint${path.sep}lib${path.sep}linter${path.sep}linter.js`) || | ||
// ESLint 5 | ||
p.includes(`eslint${path.sep}lib${path.sep}linter.js`) | ||
) | ||
} | ||
|
||
/** | ||
* Load module from the loaded ESLint. | ||
* If the loaded ESLint was not found, just returns `fallback()`. | ||
* @param {'espree' | 'eslint-scope'} name | ||
* @param { () => any } fallback | ||
*/ | ||
function getESLintModule(name, fallback) { | ||
if (!modulesCache[name]) { | ||
// Lookup the loaded eslint | ||
const linterPath = Object.keys(require.cache).find(isLinterPath) | ||
if (linterPath) { | ||
try { | ||
modulesCache[name] = createRequire(linterPath)(name) | ||
} catch (_e) { | ||
// ignore | ||
} | ||
} | ||
if (!modulesCache[name]) { | ||
modulesCache[name] = fallback() | ||
} | ||
} | ||
|
||
return modulesCache[name] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
/** | ||
* @author Yosuke Ota | ||
*/ | ||
'use strict' | ||
|
||
const { RuleTester } = require('eslint') | ||
const rule = require('../../../lib/rules/experimental-script-setup-vars') | ||
|
||
const tester = new RuleTester({ | ||
parser: require.resolve('vue-eslint-parser'), | ||
parserOptions: { ecmaVersion: 2020, sourceType: 'module' } | ||
}) | ||
|
||
tester.run('experimental-script-setup-vars', rule, { | ||
valid: [ | ||
` | ||
<script setup="props, { emit }"> | ||
import { watchEffect } from 'vue' | ||
watchEffect(() => console.log(props.msg)) | ||
emit('foo') | ||
</script>`, | ||
` | ||
<script setup> | ||
export let count = 1 | ||
</script>`, | ||
` | ||
<script> | ||
import { watchEffect } from 'vue' | ||
export default { | ||
setup (props, { emit }) { | ||
watchEffect(() => console.log(props.msg)) | ||
emit('foo') | ||
return {} | ||
} | ||
} | ||
</script>`, | ||
` | ||
<template> | ||
<div /> | ||
</template>` | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
<script setup="a - b"> | ||
</script> | ||
`, | ||
errors: [ | ||
{ | ||
message: 'Parsing error.', | ||
line: 2 | ||
} | ||
] | ||
} | ||
] | ||
}) |
Oops, something went wrong.