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 parser object to parserOptions.parser #165

Merged
merged 3 commits into from
Sep 9, 2022
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,26 @@ You can also specify an object and change the parser separately for `<script lan
}
```

When using JavaScript configuration (`.eslintrc.js`), you can also give the parser object directly.

```js
const tsParser = require("@typescript-eslint/parser")
const espree = require("espree")

module.exports = {
parser: "vue-eslint-parser",
parserOptions: {
// Single parser
parser: tsParser,
// Multiple parser
parser: {
js: espree,
ts: tsParser,
}
},
}
```

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
16 changes: 2 additions & 14 deletions src/common/espree.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
import type { ESLintExtendedProgram, ESLintProgram } from "../ast"
import type { ParserOptions } from "../common/parser-options"
import { getLinterRequire } from "./linter-require"
// @ts-expect-error -- ignore
import * as dependencyEspree from "espree"
import { lte, lt } from "semver"
import { createRequire } from "./create-require"
import path from "path"
import type { BasicParserObject } from "./parser-object"

/**
* 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 Espree = ESLintCustomParser & {
type Espree = BasicParserObject & {
latestEcmaVersion?: number
version: string
}
Expand Down
41 changes: 41 additions & 0 deletions src/common/parser-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { ESLintExtendedProgram, ESLintProgram } from "../ast"

/**
* The type of basic ESLint custom parser.
* e.g. espree
*/
export type BasicParserObject<R = ESLintProgram> = {
parse(code: string, options: any): R
parseForESLint: undefined
}
/**
* The type of ESLint custom parser enhanced for ESLint.
* e.g. @babel/eslint-parser, @typescript-eslint/parser
*/
export type EnhancedParserObject<R = ESLintExtendedProgram> = {
parseForESLint(code: string, options: any): R
parse: undefined
}

/**
* The type of ESLint (custom) parsers.
*/
export type ParserObject<R1 = ESLintExtendedProgram, R2 = ESLintProgram> =
| EnhancedParserObject<R1>
| BasicParserObject<R2>

export function isParserObject<R1, R2>(
value: ParserObject<R1, R2> | {} | undefined | null,
): value is ParserObject<R1, R2> {
return isEnhancedParserObject(value) || isBasicParserObject(value)
}
export function isEnhancedParserObject<R>(
value: EnhancedParserObject<R> | {} | undefined | null,
): value is EnhancedParserObject<R> {
return Boolean(value && typeof (value as any).parseForESLint === "function")
}
export function isBasicParserObject<R>(
value: BasicParserObject<R> | {} | undefined | null,
): value is BasicParserObject<R> {
return Boolean(value && typeof (value as any).parse === "function")
}
31 changes: 26 additions & 5 deletions src/common/parser-options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import * as path from "path"
import type { VDocumentFragment } from "../ast"
import type { CustomTemplateTokenizerConstructor } from "../html/custom-tokenizer"
import { getLang, isScriptElement, isScriptSetupElement } from "./ast-utils"
import type { ParserObject } from "./parser-object"
import { isParserObject } from "./parser-object"

export interface ParserOptions {
// vue-eslint-parser options
parser?: boolean | string
parser?:
| boolean
| string
| ParserObject
| Record<string, string | ParserObject | undefined>
vueFeatures?: {
interpolationAsNonHTML?: boolean // default true
filter?: boolean // default true
Expand Down Expand Up @@ -41,7 +48,10 @@ export interface ParserOptions {
// others
// [key: string]: any

templateTokenizer?: { [key: string]: string }
templateTokenizer?: Record<
string,
string | CustomTemplateTokenizerConstructor | undefined
>
}

export function isSFCFile(parserOptions: ParserOptions) {
Expand All @@ -55,9 +65,17 @@ export function isSFCFile(parserOptions: ParserOptions) {
* Gets the script parser name from the given parser lang.
*/
export function getScriptParser(
parser: boolean | string | Record<string, string | undefined> | undefined,
parser:
| boolean
| string
| ParserObject
| Record<string, string | ParserObject | undefined>
| undefined,
getParserLang: () => string | null | Iterable<string | null>,
): string | undefined {
): string | ParserObject | undefined {
if (isParserObject(parser)) {
return parser
}
if (parser && typeof parser === "object") {
const parserLang = getParserLang()
const parserLangs =
Expand All @@ -68,7 +86,10 @@ export function getScriptParser(
: parserLang
for (const lang of parserLangs) {
const parserForLang = lang && parser[lang]
if (typeof parserForLang === "string") {
if (
typeof parserForLang === "string" ||
isParserObject(parserForLang)
) {
return parserForLang
}
}
Expand Down
51 changes: 51 additions & 0 deletions src/html/custom-tokenizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Namespace, ParseError, Token } from "../ast"
import type { IntermediateToken } from "./intermediate-tokenizer"
import type { TokenizerState } from "./tokenizer"

export interface CustomTemplateTokenizer {
/**
* The tokenized low level tokens, excluding comments.
*/
readonly tokens: Token[]
/**
* The tokenized low level comment tokens
*/
readonly comments: Token[]
/**
* The source code text.
*/
readonly text: string
/**
* The parse errors.
*/
readonly errors: ParseError[]
/**
* The current state.
*/
state: TokenizerState
/**
* The current namespace.
*/
namespace: Namespace
/**
* The current flag of expression enabled.
*/
expressionEnabled: boolean
/**
* Get the next intermediate token.
* @returns The intermediate token or null.
*/
nextToken(): IntermediateToken | null
}

/**
* Initialize tokenizer.
* @param templateText The contents of the <template> tag.
* @param text The complete source code
* @param option The starting location of the templateText. Your token positions need to include this offset.
*/
export type CustomTemplateTokenizerConstructor = new (
templateText: string,
text: string,
option: { startingLine: number; startingColumn: number },
) => CustomTemplateTokenizer
35 changes: 22 additions & 13 deletions src/html/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ import {
} from "../common/parser-options"
import sortedIndexBy from "lodash/sortedIndexBy"
import sortedLastIndexBy from "lodash/sortedLastIndexBy"
import type {
CustomTemplateTokenizer,
CustomTemplateTokenizerConstructor,
} from "./custom-tokenizer"

const DIRECTIVE_NAME = /^(?:v-|[.:@#]).*[^.:@#]$/u
const DT_DD = /^d[dt]$/u
Expand Down Expand Up @@ -167,7 +171,7 @@ function propagateEndLocation(node: VDocumentFragment | VElement): void {
* This is not following to the HTML spec completely because Vue.js template spec is pretty different to HTML.
*/
export class Parser {
private tokenizer: IntermediateTokenizer
private tokenizer: IntermediateTokenizer | CustomTemplateTokenizer
private locationCalculator: LocationCalculatorForHtml
private baseParserOptions: ParserOptions
private isSFC: boolean
Expand Down Expand Up @@ -480,12 +484,17 @@ export class Parser {
/**
* Process the given template text token with a configured template tokenizer, based on language.
* @param token The template text token to process.
* @param lang The template language the text token should be parsed as.
* @param templateTokenizerOption The template tokenizer option.
*/
private processTemplateText(token: Text, lang: string): void {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const TemplateTokenizer = require(this.baseParserOptions
.templateTokenizer![lang])
private processTemplateText(
token: Text,
templateTokenizerOption: string | CustomTemplateTokenizerConstructor,
): void {
const TemplateTokenizer: CustomTemplateTokenizerConstructor =
typeof templateTokenizerOption === "function"
? templateTokenizerOption
: // eslint-disable-next-line @typescript-eslint/no-require-imports
require(templateTokenizerOption)
const templateTokenizer = new TemplateTokenizer(
token.value,
this.text,
Expand Down Expand Up @@ -696,13 +705,13 @@ export class Parser {
(a) => a.key.name === "lang",
)
const lang = (langAttribute?.value as VLiteral)?.value
if (
lang &&
lang !== "html" &&
this.baseParserOptions.templateTokenizer?.[lang]
) {
this.processTemplateText(token, lang)
return
if (lang && lang !== "html") {
const templateTokenizerOption =
this.baseParserOptions.templateTokenizer?.[lang]
if (templateTokenizerOption) {
this.processTemplateText(token, templateTokenizerOption)
return
}
}
}
parent.children.push({
Expand Down
14 changes: 8 additions & 6 deletions src/script/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import {
analyzeExternalReferences,
analyzeVariablesAndExternalReferences,
} from "./scope-analyzer"
import type { ESLintCustomParser } from "../common/espree"
import {
getEcmaVersionIfUseEspree,
getEspreeFromUser,
Expand All @@ -60,6 +59,8 @@ import {
} from "../script-setup/parser-options"
import { isScriptSetupElement } from "../common/ast-utils"
import type { LinesAndColumns } from "../common/lines-and-columns"
import type { ParserObject } from "../common/parser-object"
import { isEnhancedParserObject, isParserObject } from "../common/parser-object"

// [1] = aliases.
// [2] = delimiter.
Expand Down Expand Up @@ -545,15 +546,16 @@ export function parseScript(
code: string,
parserOptions: ParserOptions,
): ESLintExtendedProgram {
const parser: ESLintCustomParser =
const parser: ParserObject =
typeof parserOptions.parser === "string"
? loadParser(parserOptions.parser)
: isParserObject(parserOptions.parser)
? parserOptions.parser
: getEspreeFromEcmaVersion(parserOptions.ecmaVersion)

const result: any =
typeof parser.parseForESLint === "function"
? parser.parseForESLint(code, parserOptions)
: parser.parse(code, parserOptions)
const result: any = isEnhancedParserObject(parser)
? parser.parseForESLint(code, parserOptions)
: parser.parse(code, parserOptions)

if (result.ast != null) {
return result
Expand Down
14 changes: 6 additions & 8 deletions src/sfc/custom-block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ import { getEslintScope } from "../../common/eslint-scope"
import { getEcmaVersionIfUseEspree } from "../../common/espree"
import { fixErrorLocation, fixLocations } from "../../common/fix-locations"
import type { LocationCalculatorForHtml } from "../../common/location-calculator"
import type { ParserObject } from "../../common/parser-object"
import { isEnhancedParserObject } from "../../common/parser-object"
import type { ParserOptions } from "../../common/parser-options"
import { DEFAULT_ECMA_VERSION } from "../../script-setup/parser-options"

export interface ESLintCustomBlockParser {
parse(code: string, options: any): any
parseForESLint?(code: string, options: any): any
}
export type ESLintCustomBlockParser = ParserObject<any, any>

export type CustomBlockContext = {
getSourceCode(): SourceCode
Expand Down Expand Up @@ -181,10 +180,9 @@ function parseBlock(
parser: ESLintCustomBlockParser,
parserOptions: any,
): any {
const result: any =
typeof parser.parseForESLint === "function"
? parser.parseForESLint(code, parserOptions)
: parser.parse(code, parserOptions)
const result = isEnhancedParserObject(parser)
? parser.parseForESLint(code, parserOptions)
: parser.parse(code, parserOptions)

if (result.ast != null) {
return result
Expand Down
40 changes: 40 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,46 @@ describe("Basic tests", () => {
assert.deepStrictEqual(report[0].messages, [])
assert.deepStrictEqual(report[1].messages, [])
})

it("should notify no error with parser object with '@typescript-eslint/parser'", async () => {
const cli = new ESLint({
cwd: FIXTURE_DIR,
overrideConfig: {
env: { es6: true, node: true },
parser: PARSER_PATH,
parserOptions: {
parser: require("@typescript-eslint/parser"),
},
rules: { semi: ["error", "never"] },
},
useEslintrc: false,
})
const report = await cli.lintFiles(["typed.js"])
const messages = report[0].messages

assert.deepStrictEqual(messages, [])
})

it("should notify no error with multiple parser object with '@typescript-eslint/parser'", async () => {
const cli = new ESLint({
cwd: FIXTURE_DIR,
overrideConfig: {
env: { es6: true, node: true },
parser: PARSER_PATH,
parserOptions: {
parser: {
ts: require("@typescript-eslint/parser"),
},
},
rules: { semi: ["error", "never"] },
},
useEslintrc: false,
})
const report = await cli.lintFiles(["typed.ts", "typed.tsx"])

assert.deepStrictEqual(report[0].messages, [])
assert.deepStrictEqual(report[1].messages, [])
})
}
})

Expand Down