Skip to content

Commit

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

* update
  • Loading branch information
ota-meshi committed Sep 23, 2020
1 parent 47ade60 commit bcca364
Show file tree
Hide file tree
Showing 9 changed files with 393 additions and 1 deletion.
9 changes: 8 additions & 1 deletion docs/.vuepress/components/eslint-code-block.vue
Expand Up @@ -62,6 +62,7 @@ export default {
config() {
return {
globals: {
console: false,
// ES2015 globals
ArrayBuffer: false,
DataView: false,
Expand Down Expand Up @@ -121,8 +122,13 @@ export default {
async mounted() {
// Load linter.
const [{ default: Linter }, { parseForESLint }] = await Promise.all([
const [
{ default: Linter },
{ default: noUndefRule },
{ parseForESLint }
] = await Promise.all([
import('eslint4b/dist/linter'),
import('eslint/lib/rules/no-undef'),
import('espree').then(() => import('vue-eslint-parser'))
])
Expand All @@ -131,6 +137,7 @@ export default {
for (const ruleId of Object.keys(rules)) {
linter.defineRule(`vue/${ruleId}`, rules[ruleId])
}
linter.defineRule('no-undef', noUndefRule)
linter.defineParser('vue-eslint-parser', { parseForESLint })
}
Expand Down
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -24,6 +24,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
| Rule ID | Description | |
|:--------|:------------|:---|
| [vue/comment-directive](./comment-directive.md) | support comment-directives in `<template>` | |
| [vue/experimental-script-setup-vars](./experimental-script-setup-vars.md) | prevent variables defined in `<script setup>` to be marked as undefined | |
| [vue/jsx-uses-vars](./jsx-uses-vars.md) | prevent variables used in JSX 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
42 changes: 42 additions & 0 deletions docs/rules/experimental-script-setup-vars.md
@@ -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)
1 change: 1 addition & 0 deletions lib/configs/base.js
Expand Up @@ -16,6 +16,7 @@ module.exports = {
plugins: ['vue'],
rules: {
'vue/comment-directive': 'error',
'vue/experimental-script-setup-vars': 'error',
'vue/jsx-uses-vars': 'error'
}
}
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -25,6 +25,7 @@ module.exports = {
'dot-location': require('./rules/dot-location'),
'dot-notation': require('./rules/dot-notation'),
eqeqeq: require('./rules/eqeqeq'),
'experimental-script-setup-vars': require('./rules/experimental-script-setup-vars'),
'func-call-spacing': require('./rules/func-call-spacing'),
'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
Expand Down
228 changes: 228 additions & 0 deletions lib/rules/experimental-script-setup-vars.js
@@ -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]
}
58 changes: 58 additions & 0 deletions tests/lib/rules/experimental-script-setup-vars.js
@@ -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
}
]
}
]
})

0 comments on commit bcca364

Please sign in to comment.