Skip to content

Change parserOptions.parser to accept multiple lang parsers #116

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 4 commits into from
Jul 6, 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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,28 @@ For example:
}
```

You can also specify an object and change the parser separately for `<script lang="...">`.

```jsonc
{
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": {
// Script parser for `<script>`
"js": "espree",

// Script parser for `<script lang="ts">`
"ts": "@typescript-eslint/parser",

// Script parser for vue directives (e.g. `v-if=` or `:attribute=`)
// and vue interpolations (e.g. `{{variable}}`).
// If not specified, the parser determined by `<script lang ="...">` is used.
"<template>": "espree",
}
}
}
```

If the `parserOptions.parser` is `false`, the `vue-eslint-parser` skips parsing `<script>` tags completely.
This is useful for people who use the language ESLint community doesn't provide custom parser implementation.

Expand Down
54 changes: 54 additions & 0 deletions src/common/ast-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { VAttribute, VDirective, VElement, VNode } from "../ast"

/**
* Check whether the node is a `<script>` element.
* @param node The node to check.
* @returns `true` if the node is a `<script>` element.
*/
export function isScriptElement(node: VNode): node is VElement {
return node.type === "VElement" && node.name === "script"
}

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

/**
* Check whether the node is a `<template>` element.
* @param node The node to check.
* @returns `true` if the node is a `<template>` element.
*/
export function isTemplateElement(node: VNode): node is VElement {
return node.type === "VElement" && node.name === "template"
}

/**
* Check whether the attribute node is a `lang` attribute.
* @param attribute The attribute node to check.
* @returns `true` if the attribute node is a `lang` attribute.
*/
export function isLang(
attribute: VAttribute | VDirective,
): attribute is VAttribute {
return attribute.directive === false && attribute.key.name === "lang"
}

/**
* Get the `lang` attribute value from a given element.
* @param element The element to get.
* @param defaultLang The default value of the `lang` attribute.
* @returns The `lang` attribute value.
*/
export function getLang(element: VElement | undefined): string | null {
const langAttr = element && element.startTag.attributes.find(isLang)
const lang = langAttr && langAttr.value && langAttr.value.value
return lang || null
}
43 changes: 43 additions & 0 deletions src/common/parser-options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import * as path from "path"
import type { VDocumentFragment } from "../ast"
import { getLang, isScriptElement, isScriptSetupElement } from "./ast-utils"

export interface ParserOptions {
// vue-eslint-parser options
Expand Down Expand Up @@ -42,3 +44,44 @@ export function isSFCFile(parserOptions: ParserOptions) {
}
return path.extname(parserOptions.filePath || "unknown.vue") === ".vue"
}

/**
* Gets the script parser name from the given SFC document fragment.
*/
export function getScriptParser(
parser: boolean | string | Record<string, string | undefined> | undefined,
doc: VDocumentFragment | null,
block: "script" | "template",
): string | undefined {
if (parser && typeof parser === "object") {
if (block === "template") {
const parserForTemplate = parser["<template>"]
if (typeof parserForTemplate === "string") {
return parserForTemplate
}
}
const lang = getScriptLang()
if (lang) {
const parserForLang = parser[lang]
if (typeof parserForLang === "string") {
return parserForLang
}
}
return parser.js
}
return typeof parser === "string" ? parser : undefined

function getScriptLang() {
if (doc) {
const scripts = doc.children.filter(isScriptElement)
const script =
scripts.length === 2
? scripts.find(isScriptSetupElement)
: scripts[0]
if (script) {
return getLang(script)
}
}
return null
}
}
80 changes: 51 additions & 29 deletions src/html/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import type {
import { IntermediateTokenizer } from "./intermediate-tokenizer"
import type { Tokenizer } from "./tokenizer"
import type { ParserOptions } from "../common/parser-options"
import { isSFCFile } from "../common/parser-options"
import { isSFCFile, getScriptParser } from "../common/parser-options"

const DIRECTIVE_NAME = /^(?:v-|[.:@#]).*[^.:@#]$/u
const DT_DD = /^d[dt]$/u
Expand Down Expand Up @@ -162,11 +162,13 @@ function propagateEndLocation(node: VDocumentFragment | VElement): void {
export class Parser {
private tokenizer: IntermediateTokenizer
private locationCalculator: LocationCalculatorForHtml
private parserOptions: ParserOptions
private baseParserOptions: ParserOptions
private isSFC: boolean
private document: VDocumentFragment
private elementStack: VElement[]
private vPreElement: VElement | null
private postProcessesForScript: ((parserOptions: ParserOptions) => void)[] =
[]

/**
* The source code text.
Expand Down Expand Up @@ -243,7 +245,7 @@ export class Parser {
tokenizer.gaps,
tokenizer.lineTerminators,
)
this.parserOptions = parserOptions
this.baseParserOptions = parserOptions
this.isSFC = isSFCFile(parserOptions)
this.document = {
type: "VDocumentFragment",
Expand All @@ -260,6 +262,8 @@ export class Parser {
}
this.elementStack = []
this.vPreElement = null

this.postProcessesForScript = []
}

/**
Expand All @@ -275,6 +279,19 @@ export class Parser {
this.popElementStackUntil(0)
propagateEndLocation(this.document)

const parserOptions = {
...this.baseParserOptions,
parser: getScriptParser(
this.baseParserOptions.parser,
this.document,
"template",
),
}
for (const proc of this.postProcessesForScript) {
proc(parserOptions)
}
this.postProcessesForScript = []

return this.document
}

Expand Down Expand Up @@ -429,12 +446,14 @@ export class Parser {
attrName === "slot-scope" ||
(tagName === "template" && attrName === "scope"))
) {
convertToDirective(
this.text,
this.parserOptions,
this.locationCalculator,
node,
)
this.postProcessesForScript.push((parserOptions) => {
convertToDirective(
this.text,
parserOptions,
this.locationCalculator,
node,
)
})
return
}

Expand Down Expand Up @@ -499,19 +518,21 @@ export class Parser {
}

// Resolve references.
for (const attribute of element.startTag.attributes) {
if (attribute.directive) {
if (
attribute.key.argument != null &&
attribute.key.argument.type === "VExpressionContainer"
) {
resolveReferences(attribute.key.argument)
}
if (attribute.value != null) {
resolveReferences(attribute.value)
this.postProcessesForScript.push(() => {
for (const attribute of element.startTag.attributes) {
if (attribute.directive) {
if (
attribute.key.argument != null &&
attribute.key.argument.type === "VExpressionContainer"
) {
resolveReferences(attribute.key.argument)
}
if (attribute.value != null) {
resolveReferences(attribute.value)
}
}
}
}
})

// Check whether the self-closing is valid.
const isVoid =
Expand Down Expand Up @@ -639,17 +660,18 @@ export class Parser {
expression: null,
references: [],
}
processMustache(
this.parserOptions,
this.locationCalculator,
container,
token,
)

// Set relationship.
parent.children.push(container)

// Resolve references.
resolveReferences(container)
this.postProcessesForScript.push((parserOptions) => {
processMustache(
parserOptions,
this.locationCalculator,
container,
token,
)
// Resolve references.
resolveReferences(container)
})
}
}
Loading