Skip to content

Add support for <script setup> #110

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

Merged
merged 6 commits into from
Jul 2, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,47 @@ But, it cannot be parsed with Vue 2.
## 🎇 Usage for custom rules / plugins

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

### `defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor, options)`

*Arguments*

- `templateBodyVisitor` ... Event handlers for `<template>`.
- `scriptVisitor` ... Event handlers for `<script>` or scripts. (optional)
- `options` ... Options. (optional)
- `templateBodyTriggerSelector` ... Script AST node selector that triggers the templateBodyVisitor. Default is `"Program:exit"`. (optional)

```ts
import { AST } from "vue-eslint-parser"

export function create(context) {
return context.parserServices.defineTemplateBodyVisitor(
// Event handlers for <template>.
{
VElement(node: AST.VElement): void {
//...
}
},
// Event handlers for <script> or scripts. (optional)
{
Program(node: AST.ESLintProgram): void {
//...
}
},
// Options. (optional)
{
templateBodyTriggerSelector: "Program:exit"
}
)
}
```

## ⚠️ Known Limitations

Some rules make warnings due to the outside of `<script>` tags.
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
},
"dependencies": {
"debug": "^4.1.1",
"eslint-scope": "^5.0.0",
"eslint-scope": "^5.1.1",
"eslint-visitor-keys": "^1.1.0",
"espree": "^6.2.1",
"espree": "^8.0.0",
"esquery": "^1.4.0",
"lodash": "^4.17.15"
"lodash": "^4.17.21",
"semver": "^7.3.5"
},
"devDependencies": {
"@mysticatea/eslint-plugin": "^13.0.0",
Expand All @@ -28,6 +29,7 @@
"@types/lodash": "^4.14.120",
"@types/mocha": "^5.2.4",
"@types/node": "^10.12.21",
"@types/semver": "^7.3.6",
"@typescript-eslint/eslint-plugin": "^4.9.1",
"@typescript-eslint/parser": "^4.14.0",
"babel-eslint": "^10.0.1",
Expand All @@ -47,7 +49,6 @@
"rollup": "^1.1.2",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-sourcemaps": "^0.4.2",
"semver": "^7.3.4",
"ts-node": "^8.1.0",
"typescript": "~4.0.5",
"wait-on": "^3.2.0",
Expand Down
2 changes: 1 addition & 1 deletion scripts/update-fixtures-ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const ROOT = path.join(__dirname, "../test/fixtures/ast")
const TARGETS = fs.readdirSync(ROOT)
const PARSER_OPTIONS = {
comment: true,
ecmaVersion: 2018,
ecmaVersion: 2022,
loc: true,
range: true,
tokens: true,
Expand Down
25 changes: 25 additions & 0 deletions src/common/eslint-scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import escope from "eslint-scope"
import { getLinterRequire } from "./linter-require"
import { lte } from "semver"

let escopeCache: typeof escope | null = null

/**
* Load the newest `eslint-scope` from the loaded ESLint or dependency.
*/
export function getEslintScope(): typeof escope & {
version: string
} {
if (!escopeCache) {
escopeCache = getLinterRequire()?.("eslint-scope")
if (
!escopeCache ||
escopeCache.version == null ||
lte(escopeCache.version, escope.version)
) {
escopeCache = escope
}
}

return escopeCache
}
134 changes: 134 additions & 0 deletions src/common/espree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import type { ESLintExtendedProgram, ESLintProgram } from "../ast"
import type { ParserOptions } from "../common/parser-options"
import { getLinterRequire } from "./linter-require"
// @ts-expect-error -- ignore
import * as espree from "espree"
import { lte, lt } from "semver"

/**
* The interface of a result of ESLint custom parser.
*/
export type ESLintCustomParserResult = ESLintProgram | ESLintExtendedProgram

/**
* The interface of ESLint custom parsers.
*/
export interface ESLintCustomParser {
parse(code: string, options: any): ESLintCustomParserResult
parseForESLint?(code: string, options: any): ESLintCustomParserResult
}
type OldEspree = ESLintCustomParser & {
latestEcmaVersion?: number
version: string
}
type Espree = ESLintCustomParser & {
latestEcmaVersion: number
version: string
}
let espreeCache: OldEspree | Espree | null = null

/**
* Gets the espree that the given ecmaVersion can parse.
*/
export function getEspreeFromEcmaVersion(
ecmaVersion: ParserOptions["ecmaVersion"],
): OldEspree | Espree {
const linterEspree = getEspreeFromLinter()
if (
linterEspree.version != null &&
lte(espree.version, linterEspree.version)
) {
// linterEspree is newest
return linterEspree
}
if (ecmaVersion == null) {
return linterEspree
}
if (ecmaVersion === "latest") {
return espree
}
if (normalizeEcmaVersion(ecmaVersion) <= getLinterLatestEcmaVersion()) {
return linterEspree
}
return espree

function getLinterLatestEcmaVersion() {
if (linterEspree.latestEcmaVersion == null) {
for (const { v, latest } of [
{ v: "6.1.0", latest: 2020 },
{ v: "4.0.0", latest: 2019 },
]) {
if (lte(v, linterEspree.version)) {
return latest
}
}
return 2018
}
return normalizeEcmaVersion(linterEspree.latestEcmaVersion)
}
}

/**
* Load `espree` from the loaded ESLint.
* If the loaded ESLint was not found, just returns `require("espree")`.
*/
export function getEspreeFromLinter(): Espree | OldEspree {
if (!espreeCache) {
espreeCache = getLinterRequire()?.("espree")
if (!espreeCache) {
espreeCache = espree
}
}

return espreeCache!
}

/**
* Load the newest `espree` from the loaded ESLint or dependency.
*/
function getNewestEspree(): Espree {
const linterEspree = getEspreeFromLinter()
if (
linterEspree.version == null ||
lte(linterEspree.version, espree.version)
) {
return espree
}
return linterEspree as Espree
}

export function getEcmaVersionIfUseEspree(
parserOptions: ParserOptions,
getDefault?: (defaultVer: number) => number,
): number | undefined {
if (parserOptions.parser != null && parserOptions.parser !== "espree") {
return undefined
}

if (parserOptions.ecmaVersion === "latest") {
return normalizeEcmaVersion(getNewestEspree().latestEcmaVersion)
}
if (parserOptions.ecmaVersion == null) {
const defVer = getDefaultEcmaVersion()
return getDefault?.(defVer) ?? defVer
}
return normalizeEcmaVersion(parserOptions.ecmaVersion)
}

function getDefaultEcmaVersion(): number {
if (lt(getEspreeFromLinter().version, "9.0.0")) {
return 5
}
// Perhaps the version 9 will change the default to "latest".
return normalizeEcmaVersion(getNewestEspree().latestEcmaVersion)
}

/**
* Normalize ECMAScript version
*/
function normalizeEcmaVersion(version: number) {
if (version > 5 && version < 2015) {
return version + 2009
}
return version
}
16 changes: 11 additions & 5 deletions src/common/fix-locations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ export function fixLocations(
): void {
// There are cases which the same node instance appears twice in the tree.
// E.g. `let {a} = {}` // This `a` appears twice at `Property#key` and `Property#value`.
const traversed = new Set<Node | number[] | LocationRange>()
const traversed = new Map<Node | number[] | LocationRange, Node>()

traverseNodes(result.ast, {
visitorKeys: result.visitorKeys,

enterNode(node, parent) {
if (!traversed.has(node)) {
traversed.add(node)
traversed.set(node, node)
node.parent = parent

// `babel-eslint@8` has shared `Node#range` with multiple nodes.
Expand All @@ -45,12 +45,18 @@ export function fixLocations(
node.loc.end = locationCalculator.getLocFromIndex(
node.range[1],
)
traversed.add(node.loc)
traversed.set(node.loc, node)
} else if (node.start != null || node.end != null) {
const traversedNode = traversed.get(node.range)!
if (traversedNode.type === node.type) {
node.start = traversedNode.start
node.end = traversedNode.end
}
}
} else {
fixLocation(node, locationCalculator)
traversed.add(node.range)
traversed.add(node.loc)
traversed.set(node.range, node)
traversed.set(node.loc, node)
}
}
},
Expand Down
43 changes: 43 additions & 0 deletions src/common/linter-require.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Module from "module"
import path from "path"

const createRequire: (filename: string) => (modname: string) => any =
// Added in v12.2.0
(Module as any).createRequire ||
// Added in v10.12.0, but deprecated in v12.2.0.
// eslint-disable-next-line @mysticatea/node/no-deprecated-api
Module.createRequireFromPath ||
// Polyfill - This is not executed on the tests on node@>=10.
/* istanbul ignore next */
((modname) => {
const mod = new Module(modname)

mod.filename = modname
mod.paths = (Module as any)._nodeModulePaths(path.dirname(modname))
;(mod as any)._compile("module.exports = require;", modname)
return mod.exports
})

function isLinterPath(p: string): boolean {
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`)
)
}

export function getLinterRequire() {
// Lookup the loaded eslint
const linterPath = Object.keys(require.cache).find(isLinterPath)
if (linterPath) {
try {
return createRequire(linterPath)
} catch {
// ignore
}
}
return null
}
2 changes: 1 addition & 1 deletion src/common/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface ParserOptions {
}

// espree options
ecmaVersion?: number
ecmaVersion?: number | "latest"
sourceType?: "script" | "module"
ecmaFeatures?: { [key: string]: any }

Expand Down
11 changes: 1 addition & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { HTMLParser, HTMLTokenizer } from "./html"
import { parseScript, parseScriptElement } from "./script"
import * as services from "./parser-services"
import type { ParserOptions } from "./common/parser-options"
import { parseScriptSetupElements } from "./script-setup"
import { isScriptSetup, parseScriptSetupElements } from "./script-setup"
import { LinesAndColumns } from "./common/lines-and-columns"
import type { VElement } from "./ast"

Expand Down Expand Up @@ -71,15 +71,6 @@ function getLang(
return lang || defaultLang
}

/**
* Checks whether the given script element is `<script setup>`.
*/
function isScriptSetup(script: AST.VElement): boolean {
return script.startTag.attributes.some(
(attr) => !attr.directive && attr.key.name === "setup",
)
}

/**
* Parse the given source code.
* @param code The source code to parse.
Expand Down
Loading