Skip to content

Commit 66d424e

Browse files
authored
Add support for <script setup> (#110)
* Supports script setup * Update test * Change to respect the user parser options. * Update test * update * Update
1 parent 2f5e7fe commit 66d424e

File tree

144 files changed

+25158
-184
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

144 files changed

+25158
-184
lines changed

Diff for: README.md

+35-1
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,47 @@ But, it cannot be parsed with Vue 2.
170170
## 🎇 Usage for custom rules / plugins
171171

172172
- This parser provides `parserServices` to traverse `<template>`.
173-
- `defineTemplateBodyVisitor(templateVisitor, scriptVisitor)` ... returns ESLint visitor to traverse `<template>`.
173+
- `defineTemplateBodyVisitor(templateVisitor, scriptVisitor, options)` ... returns ESLint visitor to traverse `<template>`.
174174
- `getTemplateBodyTokenStore()` ... returns ESLint `TokenStore` to get the tokens of `<template>`.
175175
- `getDocumentFragment()` ... returns the root `VDocumentFragment`.
176176
- `defineCustomBlocksVisitor(context, customParser, rule, scriptVisitor)` ... returns ESLint visitor that parses and traverses the contents of the custom block.
177177
- [ast.md](./docs/ast.md) is `<template>` AST specification.
178178
- [mustache-interpolation-spacing.js](https://github.com/vuejs/eslint-plugin-vue/blob/b434ff99d37f35570fa351681e43ba2cf5746db3/lib/rules/mustache-interpolation-spacing.js) is an example.
179179

180+
### `defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor, options)`
181+
182+
*Arguments*
183+
184+
- `templateBodyVisitor` ... Event handlers for `<template>`.
185+
- `scriptVisitor` ... Event handlers for `<script>` or scripts. (optional)
186+
- `options` ... Options. (optional)
187+
- `templateBodyTriggerSelector` ... Script AST node selector that triggers the templateBodyVisitor. Default is `"Program:exit"`. (optional)
188+
189+
```ts
190+
import { AST } from "vue-eslint-parser"
191+
192+
export function create(context) {
193+
return context.parserServices.defineTemplateBodyVisitor(
194+
// Event handlers for <template>.
195+
{
196+
VElement(node: AST.VElement): void {
197+
//...
198+
}
199+
},
200+
// Event handlers for <script> or scripts. (optional)
201+
{
202+
Program(node: AST.ESLintProgram): void {
203+
//...
204+
}
205+
},
206+
// Options. (optional)
207+
{
208+
templateBodyTriggerSelector: "Program:exit"
209+
}
210+
)
211+
}
212+
```
213+
180214
## ⚠️ Known Limitations
181215

182216
Some rules make warnings due to the outside of `<script>` tags.

Diff for: package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
},
1515
"dependencies": {
1616
"debug": "^4.1.1",
17-
"eslint-scope": "^5.0.0",
17+
"eslint-scope": "^5.1.1",
1818
"eslint-visitor-keys": "^1.1.0",
19-
"espree": "^6.2.1",
19+
"espree": "^8.0.0",
2020
"esquery": "^1.4.0",
21-
"lodash": "^4.17.15"
21+
"lodash": "^4.17.21",
22+
"semver": "^7.3.5"
2223
},
2324
"devDependencies": {
2425
"@mysticatea/eslint-plugin": "^13.0.0",
@@ -28,6 +29,7 @@
2829
"@types/lodash": "^4.14.120",
2930
"@types/mocha": "^5.2.4",
3031
"@types/node": "^10.12.21",
32+
"@types/semver": "^7.3.6",
3133
"@typescript-eslint/eslint-plugin": "^4.9.1",
3234
"@typescript-eslint/parser": "^4.14.0",
3335
"babel-eslint": "^10.0.1",
@@ -47,7 +49,6 @@
4749
"rollup": "^1.1.2",
4850
"rollup-plugin-node-resolve": "^4.0.0",
4951
"rollup-plugin-sourcemaps": "^0.4.2",
50-
"semver": "^7.3.4",
5152
"ts-node": "^8.1.0",
5253
"typescript": "~4.0.5",
5354
"wait-on": "^3.2.0",

Diff for: scripts/update-fixtures-ast.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const ROOT = path.join(__dirname, "../test/fixtures/ast")
2222
const TARGETS = fs.readdirSync(ROOT)
2323
const PARSER_OPTIONS = {
2424
comment: true,
25-
ecmaVersion: 2018,
25+
ecmaVersion: 2022,
2626
loc: true,
2727
range: true,
2828
tokens: true,

Diff for: src/common/eslint-scope.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import escope from "eslint-scope"
2+
import { getLinterRequire } from "./linter-require"
3+
import { lte } from "semver"
4+
5+
let escopeCache: typeof escope | null = null
6+
7+
/**
8+
* Load the newest `eslint-scope` from the loaded ESLint or dependency.
9+
*/
10+
export function getEslintScope(): typeof escope & {
11+
version: string
12+
} {
13+
if (!escopeCache) {
14+
escopeCache = getLinterRequire()?.("eslint-scope")
15+
if (
16+
!escopeCache ||
17+
escopeCache.version == null ||
18+
lte(escopeCache.version, escope.version)
19+
) {
20+
escopeCache = escope
21+
}
22+
}
23+
24+
return escopeCache
25+
}

Diff for: src/common/espree.ts

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import type { ESLintExtendedProgram, ESLintProgram } from "../ast"
2+
import type { ParserOptions } from "../common/parser-options"
3+
import { getLinterRequire } from "./linter-require"
4+
// @ts-expect-error -- ignore
5+
import * as espree from "espree"
6+
import { lte, lt } from "semver"
7+
8+
/**
9+
* The interface of a result of ESLint custom parser.
10+
*/
11+
export type ESLintCustomParserResult = ESLintProgram | ESLintExtendedProgram
12+
13+
/**
14+
* The interface of ESLint custom parsers.
15+
*/
16+
export interface ESLintCustomParser {
17+
parse(code: string, options: any): ESLintCustomParserResult
18+
parseForESLint?(code: string, options: any): ESLintCustomParserResult
19+
}
20+
type OldEspree = ESLintCustomParser & {
21+
latestEcmaVersion?: number
22+
version: string
23+
}
24+
type Espree = ESLintCustomParser & {
25+
latestEcmaVersion: number
26+
version: string
27+
}
28+
let espreeCache: OldEspree | Espree | null = null
29+
30+
/**
31+
* Gets the espree that the given ecmaVersion can parse.
32+
*/
33+
export function getEspreeFromEcmaVersion(
34+
ecmaVersion: ParserOptions["ecmaVersion"],
35+
): OldEspree | Espree {
36+
const linterEspree = getEspreeFromLinter()
37+
if (
38+
linterEspree.version != null &&
39+
lte(espree.version, linterEspree.version)
40+
) {
41+
// linterEspree is newest
42+
return linterEspree
43+
}
44+
if (ecmaVersion == null) {
45+
return linterEspree
46+
}
47+
if (ecmaVersion === "latest") {
48+
return espree
49+
}
50+
if (normalizeEcmaVersion(ecmaVersion) <= getLinterLatestEcmaVersion()) {
51+
return linterEspree
52+
}
53+
return espree
54+
55+
function getLinterLatestEcmaVersion() {
56+
if (linterEspree.latestEcmaVersion == null) {
57+
for (const { v, latest } of [
58+
{ v: "6.1.0", latest: 2020 },
59+
{ v: "4.0.0", latest: 2019 },
60+
]) {
61+
if (lte(v, linterEspree.version)) {
62+
return latest
63+
}
64+
}
65+
return 2018
66+
}
67+
return normalizeEcmaVersion(linterEspree.latestEcmaVersion)
68+
}
69+
}
70+
71+
/**
72+
* Load `espree` from the loaded ESLint.
73+
* If the loaded ESLint was not found, just returns `require("espree")`.
74+
*/
75+
export function getEspreeFromLinter(): Espree | OldEspree {
76+
if (!espreeCache) {
77+
espreeCache = getLinterRequire()?.("espree")
78+
if (!espreeCache) {
79+
espreeCache = espree
80+
}
81+
}
82+
83+
return espreeCache!
84+
}
85+
86+
/**
87+
* Load the newest `espree` from the loaded ESLint or dependency.
88+
*/
89+
function getNewestEspree(): Espree {
90+
const linterEspree = getEspreeFromLinter()
91+
if (
92+
linterEspree.version == null ||
93+
lte(linterEspree.version, espree.version)
94+
) {
95+
return espree
96+
}
97+
return linterEspree as Espree
98+
}
99+
100+
export function getEcmaVersionIfUseEspree(
101+
parserOptions: ParserOptions,
102+
getDefault?: (defaultVer: number) => number,
103+
): number | undefined {
104+
if (parserOptions.parser != null && parserOptions.parser !== "espree") {
105+
return undefined
106+
}
107+
108+
if (parserOptions.ecmaVersion === "latest") {
109+
return normalizeEcmaVersion(getNewestEspree().latestEcmaVersion)
110+
}
111+
if (parserOptions.ecmaVersion == null) {
112+
const defVer = getDefaultEcmaVersion()
113+
return getDefault?.(defVer) ?? defVer
114+
}
115+
return normalizeEcmaVersion(parserOptions.ecmaVersion)
116+
}
117+
118+
function getDefaultEcmaVersion(): number {
119+
if (lt(getEspreeFromLinter().version, "9.0.0")) {
120+
return 5
121+
}
122+
// Perhaps the version 9 will change the default to "latest".
123+
return normalizeEcmaVersion(getNewestEspree().latestEcmaVersion)
124+
}
125+
126+
/**
127+
* Normalize ECMAScript version
128+
*/
129+
function normalizeEcmaVersion(version: number) {
130+
if (version > 5 && version < 2015) {
131+
return version + 2009
132+
}
133+
return version
134+
}

Diff for: src/common/fix-locations.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ export function fixLocations(
2323
): void {
2424
// There are cases which the same node instance appears twice in the tree.
2525
// E.g. `let {a} = {}` // This `a` appears twice at `Property#key` and `Property#value`.
26-
const traversed = new Set<Node | number[] | LocationRange>()
26+
const traversed = new Map<Node | number[] | LocationRange, Node>()
2727

2828
traverseNodes(result.ast, {
2929
visitorKeys: result.visitorKeys,
3030

3131
enterNode(node, parent) {
3232
if (!traversed.has(node)) {
33-
traversed.add(node)
33+
traversed.set(node, node)
3434
node.parent = parent
3535

3636
// `babel-eslint@8` has shared `Node#range` with multiple nodes.
@@ -45,12 +45,18 @@ export function fixLocations(
4545
node.loc.end = locationCalculator.getLocFromIndex(
4646
node.range[1],
4747
)
48-
traversed.add(node.loc)
48+
traversed.set(node.loc, node)
49+
} else if (node.start != null || node.end != null) {
50+
const traversedNode = traversed.get(node.range)!
51+
if (traversedNode.type === node.type) {
52+
node.start = traversedNode.start
53+
node.end = traversedNode.end
54+
}
4955
}
5056
} else {
5157
fixLocation(node, locationCalculator)
52-
traversed.add(node.range)
53-
traversed.add(node.loc)
58+
traversed.set(node.range, node)
59+
traversed.set(node.loc, node)
5460
}
5561
}
5662
},

Diff for: src/common/linter-require.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Module from "module"
2+
import path from "path"
3+
4+
const createRequire: (filename: string) => (modname: string) => any =
5+
// Added in v12.2.0
6+
(Module as any).createRequire ||
7+
// Added in v10.12.0, but deprecated in v12.2.0.
8+
// eslint-disable-next-line @mysticatea/node/no-deprecated-api
9+
Module.createRequireFromPath ||
10+
// Polyfill - This is not executed on the tests on node@>=10.
11+
/* istanbul ignore next */
12+
((modname) => {
13+
const mod = new Module(modname)
14+
15+
mod.filename = modname
16+
mod.paths = (Module as any)._nodeModulePaths(path.dirname(modname))
17+
;(mod as any)._compile("module.exports = require;", modname)
18+
return mod.exports
19+
})
20+
21+
function isLinterPath(p: string): boolean {
22+
return (
23+
// ESLint 6 and above
24+
p.includes(
25+
`eslint${path.sep}lib${path.sep}linter${path.sep}linter.js`,
26+
) ||
27+
// ESLint 5
28+
p.includes(`eslint${path.sep}lib${path.sep}linter.js`)
29+
)
30+
}
31+
32+
export function getLinterRequire() {
33+
// Lookup the loaded eslint
34+
const linterPath = Object.keys(require.cache).find(isLinterPath)
35+
if (linterPath) {
36+
try {
37+
return createRequire(linterPath)
38+
} catch {
39+
// ignore
40+
}
41+
}
42+
return null
43+
}

Diff for: src/common/parser-options.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface ParserOptions {
99
}
1010

1111
// espree options
12-
ecmaVersion?: number
12+
ecmaVersion?: number | "latest"
1313
sourceType?: "script" | "module"
1414
ecmaFeatures?: { [key: string]: any }
1515

Diff for: src/index.ts

+1-10
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { HTMLParser, HTMLTokenizer } from "./html"
1010
import { parseScript, parseScriptElement } from "./script"
1111
import * as services from "./parser-services"
1212
import type { ParserOptions } from "./common/parser-options"
13-
import { parseScriptSetupElements } from "./script-setup"
13+
import { isScriptSetup, parseScriptSetupElements } from "./script-setup"
1414
import { LinesAndColumns } from "./common/lines-and-columns"
1515
import type { VElement } from "./ast"
1616

@@ -71,15 +71,6 @@ function getLang(
7171
return lang || defaultLang
7272
}
7373

74-
/**
75-
* Checks whether the given script element is `<script setup>`.
76-
*/
77-
function isScriptSetup(script: AST.VElement): boolean {
78-
return script.startTag.attributes.some(
79-
(attr) => !attr.directive && attr.key.name === "setup",
80-
)
81-
}
82-
8374
/**
8475
* Parse the given source code.
8576
* @param code The source code to parse.

0 commit comments

Comments
 (0)