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 support for ESLint v8 (beta). #1610

Merged
merged 9 commits into from Aug 27, 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
38 changes: 38 additions & 0 deletions .circleci/config.yml
Expand Up @@ -4,6 +4,8 @@ workflows:
jobs:
- node-v8
- node-v10
- eslint-v7
- eslint-v8
- node-v12
- node-v14
- lint
Expand Down Expand Up @@ -54,6 +56,42 @@ jobs:
<<: *node-base
docker:
- image: node:10
eslint-v7:
docker:
- image: node:10
steps:
- run:
name: Versions
command: npm version
- checkout
- run:
name: Install eslint@7
command: |
npm install --save-exact eslint@7
- run:
name: Install dependencies
command: npm install
- run:
name: Test
command: npm test
eslint-v8:
docker:
- image: node:14
steps:
- run:
name: Versions
command: npm version
- checkout
- run:
name: Install eslint@8
command: |
npm install --save-exact eslint@^8.0.0-0
- run:
name: Install dependencies
command: npm install
- run:
name: Test
command: npm test
node-v12:
<<: *node-base
docker:
Expand Down
22 changes: 11 additions & 11 deletions docs/user-guide/README.md
Expand Up @@ -25,6 +25,8 @@ yarn add -D eslint eslint-plugin-vue
- ESLint v6.2.0 and above
- Node.js v8.10.0 and above

We have started supporting ESLint v8.0.0 beta, but note that beta support will be dropped once the stable version is released.

:::

## :book: Usage
Expand Down Expand Up @@ -93,13 +95,13 @@ If you installed [@vue/cli-plugin-eslint](https://github.com/vuejs/vue-cli/tree/

### How to use a custom parser?

If you want to use custom parsers such as [babel-eslint](https://www.npmjs.com/package/babel-eslint) or [@typescript-eslint/parser](https://www.npmjs.com/package/@typescript-eslint/parser), you have to use the `parserOptions.parser` option instead of the `parser` option. Because this plugin requires [vue-eslint-parser](https://www.npmjs.com/package/vue-eslint-parser) to parse `.vue` files, this plugin doesn't work if you overwrite the `parser` option.
If you want to use custom parsers such as [@babel/eslint-parser](https://www.npmjs.com/package/@babel/eslint-parser) or [@typescript-eslint/parser](https://www.npmjs.com/package/@typescript-eslint/parser), you have to use the `parserOptions.parser` option instead of the `parser` option. Because this plugin requires [vue-eslint-parser](https://www.npmjs.com/package/vue-eslint-parser) to parse `.vue` files, this plugin doesn't work if you overwrite the `parser` option.

```diff
- "parser": "babel-eslint",
- "parser": "@typescript-eslint/parser",
+ "parser": "vue-eslint-parser",
"parserOptions": {
+ "parser": "babel-eslint",
+ "parser": "@typescript-eslint/parser",
"sourceType": "module"
}
```
Expand Down Expand Up @@ -238,13 +240,13 @@ Make sure you have one of the following settings in your **.eslintrc**:
- `"extends": ["plugin:vue/vue3-recommended"]`
- `"extends": ["plugin:vue/base"]`

If you already use another parser (e.g. `"parser": "babel-eslint"`), please move it into `parserOptions`, so it doesn't collide with the `vue-eslint-parser` used by this plugin's configuration:
If you already use another parser (e.g. `"parser": "@typescript-eslint/parser"`), please move it into `parserOptions`, so it doesn't collide with the `vue-eslint-parser` used by this plugin's configuration:

```diff
- "parser": "babel-eslint",
- "parser": "@typescript-eslint/parser",
+ "parser": "vue-eslint-parser",
"parserOptions": {
+ "parser": "babel-eslint",
+ "parser": "@typescript-eslint/parser",
"ecmaVersion": 2020,
"sourceType": "module"
}
Expand Down Expand Up @@ -331,7 +333,7 @@ Note that you cannot use angle-bracket type assertion style (`var x = <foo>bar;`
- Turning off the rule in the ESLint configuration file does not ignore the warning.
- Using the `<!-- eslint-disable -->` comment does not suppress warnings.
- Duplicate warnings are displayed.
- Used `babel-eslint`, but the template still show `vue/no-parsing-error` warnings.
- Used `@babel/eslint-parser`, but the template still show `vue/no-parsing-error` warnings.

You need to turn off Vetur's template validation by adding `vetur.validation.template: false` to your `.vscode/settings.json`.

Expand Down Expand Up @@ -398,20 +400,18 @@ module.exports = {

However, note that the AST generated by `espree` v8+ may not work well with some rules of `ESLint` v7.x.

<!--
##### Using ESLint >= v8.x

You need to specify `2022` for `parserOptions.ecmaVersion`.
You need to specify `2022` or `"latest"` for `parserOptions.ecmaVersion`.

```js
module.exports = {
parserOptions: {
ecmaVersion: 2022,
ecmaVersion: 'latest',
sourceType: 'module'
},
}
```
-->

#### Other Problems

Expand Down
2 changes: 1 addition & 1 deletion lib/utils/indent-common.js
Expand Up @@ -1331,7 +1331,7 @@ module.exports.defineVisitor = function create(
setOffset([fromToken, nameToken], 1, exportToken)
}
} else {
// maybe babel-eslint
// maybe babel parser
}
}
},
Expand Down
111 changes: 110 additions & 1 deletion lib/utils/index.js
Expand Up @@ -50,7 +50,7 @@ const VUE3_BUILTIN_COMPONENT_NAMES = new Set(
)
const path = require('path')
const vueEslintParser = require('vue-eslint-parser')
const traverseNodes = vueEslintParser.AST.traverseNodes
const { traverseNodes, getFallbackKeys } = vueEslintParser.AST
const { findVariable } = require('eslint-utils')
const {
getComponentPropsFromTypeDefine,
Expand Down Expand Up @@ -155,11 +155,77 @@ function wrapContextToOverrideTokenMethods(context, tokenStore, options) {
}
})

const containerScopes = new WeakMap()

/**
* @param {ASTNode} node
*/
function getContainerScope(node) {
const exprContainer = getVExpressionContainer(node)
if (!exprContainer) {
return null
}
const cache = containerScopes.get(exprContainer)
if (cache) {
return cache
}
const programNode = eslintSourceCode.ast
const parserOptions = context.parserOptions || {}
const ecmaFeatures = parserOptions.ecmaFeatures || {}
const ecmaVersion = parserOptions.ecmaVersion || 2020
const sourceType = programNode.sourceType
try {
const eslintScope = createRequire(require.resolve('eslint'))(
'eslint-scope'
)
const expStmt = new Proxy(exprContainer, {
get(_object, key) {
if (key === 'type') {
return 'ExpressionStatement'
}
// @ts-expect-error
return exprContainer[key]
}
})
const scopeProgram = new Proxy(programNode, {
get(_object, key) {
if (key === 'body') {
return [expStmt]
}
// @ts-expect-error
return programNode[key]
}
})
const scope = eslintScope.analyze(scopeProgram, {
ignoreEval: true,
nodejsScope: false,
impliedStrict: ecmaFeatures.impliedStrict,
ecmaVersion,
sourceType,
fallback: getFallbackKeys
})
containerScopes.set(exprContainer, scope)
return scope
} catch (e) {
// ignore
// console.log(e)
}

return null
}
return {
// @ts-expect-error
__proto__: context,
getSourceCode() {
return sourceCode
},
getDeclaredVariables(node) {
const scope = getContainerScope(node)
if (scope) {
return scope.getDeclaredVariables(node)
}

return context.getDeclaredVariables(node)
}
}
}
Expand Down Expand Up @@ -1806,6 +1872,36 @@ function isDef(v) {
return v != null
}

// ------------------------------------------------------------------------------
// Nodejs Helpers
// ------------------------------------------------------------------------------
/**
* @param {String} filename
*/
function createRequire(filename) {
const Module = require('module')
const moduleCreateRequire =
// 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
}
return moduleCreateRequire(filename)
}

// ------------------------------------------------------------------------------
// Rule Helpers
// ------------------------------------------------------------------------------
Expand Down Expand Up @@ -2121,6 +2217,19 @@ function getStringLiteralValue(node, stringOnly) {
}
return null
}
/**
* Gets the VExpressionContainer of a given node.
* @param {ASTNode} node - The node to get.
* @return {VExpressionContainer|null}
*/
function getVExpressionContainer(node) {
/** @type {ASTNode | null} */
let n = node
while (n && n.type !== 'VExpressionContainer') {
n = n.parent
}
return n
}

// ------------------------------------------------------------------------------
// Vue Helpers
Expand Down
3 changes: 1 addition & 2 deletions package.json
Expand Up @@ -50,7 +50,7 @@
"node": ">=8.10"
},
"peerDependencies": {
"eslint": "^6.2.0 || ^7.0.0"
"eslint": "^6.2.0 || ^7.0.0 || ^8.0.0-0"
},
"dependencies": {
"eslint-utils": "^2.1.0",
Expand All @@ -66,7 +66,6 @@
"@types/semver": "^7.2.0",
"@typescript-eslint/parser": "^4.28.0",
"@vuepress/plugin-pwa": "^1.4.1",
"babel-eslint": "^10.1.0",
"env-cmd": "^10.1.0",
"eslint": "^7.0.0",
"eslint-config-prettier": "^6.11.0",
Expand Down
33 changes: 24 additions & 9 deletions tests/eslint-compat.js
Expand Up @@ -16,24 +16,39 @@ function getESLintClassForV6() {
/** @param {eslint.ESLint.Options} options */
constructor(options) {
const {
overrideConfig: { plugins, globals, ...overrideConfig },
overrideConfig: { plugins, globals, rules, ...overrideConfig } = {
plugins: [],
globals: {},
rules: {}
},
fix,
reportUnusedDisableDirectives,
plugins: pluginsMap,
...otherOptions
} = options
this.engine = new eslint.CLIEngine({
} = options || {}
/** @type {eslint.CLIEngine.Options} */
const newOptions = {
fix: Boolean(fix),
reportUnusedDisableDirectives: reportUnusedDisableDirectives
? reportUnusedDisableDirectives !== 'off'
: undefined,
...otherOptions,

globals: globals
? Object.keys(globals).filter((n) => globals[n])
: undefined,
...otherOptions,
...overrideConfig,
plugins: plugins || [],
reportUnusedDisableDirectives: reportUnusedDisableDirectives
? reportUnusedDisableDirectives !== 'off'
: undefined
})
rules: rules
? Object.entries(rules).reduce((o, [ruleId, opt]) => {
if (opt) {
o[ruleId] = opt
}
return o
}, /** @type {NonNullable<eslint.CLIEngine.Options["rules"]>}*/ ({}))
: undefined,
...overrideConfig
}
this.engine = new eslint.CLIEngine(newOptions)

for (const [name, plugin] of Object.entries(pluginsMap || {})) {
this.engine.addPlugin(name, plugin)
Expand Down
5 changes: 1 addition & 4 deletions tests/lib/rules-without-vue-eslint-parser.js
Expand Up @@ -5,7 +5,6 @@
'use strict'

const Linter = require('eslint').Linter
const parser = require('babel-eslint')
const rules = require('../..').rules
const assert = require('assert')

Expand All @@ -18,13 +17,11 @@ describe("Don't crash even if without vue-eslint-parser.", () => {
it(ruleId, () => {
const linter = new Linter()
const config = {
parser: 'babel-eslint',
parserOptions: { ecmaVersion: 2015 },
parserOptions: { ecmaVersion: 2015, ecmaFeatures: { jsx: true } },
rules: {
[ruleId]: 'error'
}
}
linter.defineParser('babel-eslint', parser)
linter.defineRule(ruleId, rules[key])
const resultVue = linter.verifyAndFix(code, config, 'test.vue')
for (const { message } of resultVue.messages) {
Expand Down
14 changes: 14 additions & 0 deletions tests/lib/rules/camelcase.js
Expand Up @@ -52,6 +52,20 @@ tester.run('camelcase', rule, {
line: 4
}
]
},
{
code: `
<template>
<div @click="
const { my_pref } = $event
" />
</template>`,
errors: [
{
message: "Identifier 'my_pref' is not in camel case.",
line: 4
}
]
}
]
})