Skip to content

Commit 9daf014

Browse files
authored
Add support for parser object to parserOptions.parser (#165)
* Add support for parser object to `parserOptions.parser` * add comments * update
1 parent b70caf5 commit 9daf014

File tree

9 files changed

+216
-46
lines changed

9 files changed

+216
-46
lines changed

Diff for: README.md

+20
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ You can also specify an object and change the parser separately for `<script lan
106106
}
107107
```
108108

109+
When using JavaScript configuration (`.eslintrc.js`), you can also give the parser object directly.
110+
111+
```js
112+
const tsParser = require("@typescript-eslint/parser")
113+
const espree = require("espree")
114+
115+
module.exports = {
116+
parser: "vue-eslint-parser",
117+
parserOptions: {
118+
// Single parser
119+
parser: tsParser,
120+
// Multiple parser
121+
parser: {
122+
js: espree,
123+
ts: tsParser,
124+
}
125+
},
126+
}
127+
```
128+
109129
If the `parserOptions.parser` is `false`, the `vue-eslint-parser` skips parsing `<script>` tags completely.
110130
This is useful for people who use the language ESLint community doesn't provide custom parser implementation.
111131

Diff for: src/common/espree.ts

+2-14
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,13 @@
1-
import type { ESLintExtendedProgram, ESLintProgram } from "../ast"
21
import type { ParserOptions } from "../common/parser-options"
32
import { getLinterRequire } from "./linter-require"
43
// @ts-expect-error -- ignore
54
import * as dependencyEspree from "espree"
65
import { lte, lt } from "semver"
76
import { createRequire } from "./create-require"
87
import path from "path"
8+
import type { BasicParserObject } from "./parser-object"
99

10-
/**
11-
* The interface of a result of ESLint custom parser.
12-
*/
13-
export type ESLintCustomParserResult = ESLintProgram | ESLintExtendedProgram
14-
15-
/**
16-
* The interface of ESLint custom parsers.
17-
*/
18-
export interface ESLintCustomParser {
19-
parse(code: string, options: any): ESLintCustomParserResult
20-
parseForESLint?(code: string, options: any): ESLintCustomParserResult
21-
}
22-
type Espree = ESLintCustomParser & {
10+
type Espree = BasicParserObject & {
2311
latestEcmaVersion?: number
2412
version: string
2513
}

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

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { ESLintExtendedProgram, ESLintProgram } from "../ast"
2+
3+
/**
4+
* The type of basic ESLint custom parser.
5+
* e.g. espree
6+
*/
7+
export type BasicParserObject<R = ESLintProgram> = {
8+
parse(code: string, options: any): R
9+
parseForESLint: undefined
10+
}
11+
/**
12+
* The type of ESLint custom parser enhanced for ESLint.
13+
* e.g. @babel/eslint-parser, @typescript-eslint/parser
14+
*/
15+
export type EnhancedParserObject<R = ESLintExtendedProgram> = {
16+
parseForESLint(code: string, options: any): R
17+
parse: undefined
18+
}
19+
20+
/**
21+
* The type of ESLint (custom) parsers.
22+
*/
23+
export type ParserObject<R1 = ESLintExtendedProgram, R2 = ESLintProgram> =
24+
| EnhancedParserObject<R1>
25+
| BasicParserObject<R2>
26+
27+
export function isParserObject<R1, R2>(
28+
value: ParserObject<R1, R2> | {} | undefined | null,
29+
): value is ParserObject<R1, R2> {
30+
return isEnhancedParserObject(value) || isBasicParserObject(value)
31+
}
32+
export function isEnhancedParserObject<R>(
33+
value: EnhancedParserObject<R> | {} | undefined | null,
34+
): value is EnhancedParserObject<R> {
35+
return Boolean(value && typeof (value as any).parseForESLint === "function")
36+
}
37+
export function isBasicParserObject<R>(
38+
value: BasicParserObject<R> | {} | undefined | null,
39+
): value is BasicParserObject<R> {
40+
return Boolean(value && typeof (value as any).parse === "function")
41+
}

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

+26-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import * as path from "path"
22
import type { VDocumentFragment } from "../ast"
3+
import type { CustomTemplateTokenizerConstructor } from "../html/custom-tokenizer"
34
import { getLang, isScriptElement, isScriptSetupElement } from "./ast-utils"
5+
import type { ParserObject } from "./parser-object"
6+
import { isParserObject } from "./parser-object"
47

58
export interface ParserOptions {
69
// vue-eslint-parser options
7-
parser?: boolean | string
10+
parser?:
11+
| boolean
12+
| string
13+
| ParserObject
14+
| Record<string, string | ParserObject | undefined>
815
vueFeatures?: {
916
interpolationAsNonHTML?: boolean // default true
1017
filter?: boolean // default true
@@ -41,7 +48,10 @@ export interface ParserOptions {
4148
// others
4249
// [key: string]: any
4350

44-
templateTokenizer?: { [key: string]: string }
51+
templateTokenizer?: Record<
52+
string,
53+
string | CustomTemplateTokenizerConstructor | undefined
54+
>
4555
}
4656

4757
export function isSFCFile(parserOptions: ParserOptions) {
@@ -55,9 +65,17 @@ export function isSFCFile(parserOptions: ParserOptions) {
5565
* Gets the script parser name from the given parser lang.
5666
*/
5767
export function getScriptParser(
58-
parser: boolean | string | Record<string, string | undefined> | undefined,
68+
parser:
69+
| boolean
70+
| string
71+
| ParserObject
72+
| Record<string, string | ParserObject | undefined>
73+
| undefined,
5974
getParserLang: () => string | null | Iterable<string | null>,
60-
): string | undefined {
75+
): string | ParserObject | undefined {
76+
if (isParserObject(parser)) {
77+
return parser
78+
}
6179
if (parser && typeof parser === "object") {
6280
const parserLang = getParserLang()
6381
const parserLangs =
@@ -68,7 +86,10 @@ export function getScriptParser(
6886
: parserLang
6987
for (const lang of parserLangs) {
7088
const parserForLang = lang && parser[lang]
71-
if (typeof parserForLang === "string") {
89+
if (
90+
typeof parserForLang === "string" ||
91+
isParserObject(parserForLang)
92+
) {
7293
return parserForLang
7394
}
7495
}

Diff for: src/html/custom-tokenizer.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { Namespace, ParseError, Token } from "../ast"
2+
import type { IntermediateToken } from "./intermediate-tokenizer"
3+
import type { TokenizerState } from "./tokenizer"
4+
5+
export interface CustomTemplateTokenizer {
6+
/**
7+
* The tokenized low level tokens, excluding comments.
8+
*/
9+
readonly tokens: Token[]
10+
/**
11+
* The tokenized low level comment tokens
12+
*/
13+
readonly comments: Token[]
14+
/**
15+
* The source code text.
16+
*/
17+
readonly text: string
18+
/**
19+
* The parse errors.
20+
*/
21+
readonly errors: ParseError[]
22+
/**
23+
* The current state.
24+
*/
25+
state: TokenizerState
26+
/**
27+
* The current namespace.
28+
*/
29+
namespace: Namespace
30+
/**
31+
* The current flag of expression enabled.
32+
*/
33+
expressionEnabled: boolean
34+
/**
35+
* Get the next intermediate token.
36+
* @returns The intermediate token or null.
37+
*/
38+
nextToken(): IntermediateToken | null
39+
}
40+
41+
/**
42+
* Initialize tokenizer.
43+
* @param templateText The contents of the <template> tag.
44+
* @param text The complete source code
45+
* @param option The starting location of the templateText. Your token positions need to include this offset.
46+
*/
47+
export type CustomTemplateTokenizerConstructor = new (
48+
templateText: string,
49+
text: string,
50+
option: { startingLine: number; startingColumn: number },
51+
) => CustomTemplateTokenizer

Diff for: src/html/parser.ts

+22-13
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ import {
5454
} from "../common/parser-options"
5555
import sortedIndexBy from "lodash/sortedIndexBy"
5656
import sortedLastIndexBy from "lodash/sortedLastIndexBy"
57+
import type {
58+
CustomTemplateTokenizer,
59+
CustomTemplateTokenizerConstructor,
60+
} from "./custom-tokenizer"
5761

5862
const DIRECTIVE_NAME = /^(?:v-|[.:@#]).*[^.:@#]$/u
5963
const DT_DD = /^d[dt]$/u
@@ -167,7 +171,7 @@ function propagateEndLocation(node: VDocumentFragment | VElement): void {
167171
* This is not following to the HTML spec completely because Vue.js template spec is pretty different to HTML.
168172
*/
169173
export class Parser {
170-
private tokenizer: IntermediateTokenizer
174+
private tokenizer: IntermediateTokenizer | CustomTemplateTokenizer
171175
private locationCalculator: LocationCalculatorForHtml
172176
private baseParserOptions: ParserOptions
173177
private isSFC: boolean
@@ -480,12 +484,17 @@ export class Parser {
480484
/**
481485
* Process the given template text token with a configured template tokenizer, based on language.
482486
* @param token The template text token to process.
483-
* @param lang The template language the text token should be parsed as.
487+
* @param templateTokenizerOption The template tokenizer option.
484488
*/
485-
private processTemplateText(token: Text, lang: string): void {
486-
// eslint-disable-next-line @typescript-eslint/no-require-imports
487-
const TemplateTokenizer = require(this.baseParserOptions
488-
.templateTokenizer![lang])
489+
private processTemplateText(
490+
token: Text,
491+
templateTokenizerOption: string | CustomTemplateTokenizerConstructor,
492+
): void {
493+
const TemplateTokenizer: CustomTemplateTokenizerConstructor =
494+
typeof templateTokenizerOption === "function"
495+
? templateTokenizerOption
496+
: // eslint-disable-next-line @typescript-eslint/no-require-imports
497+
require(templateTokenizerOption)
489498
const templateTokenizer = new TemplateTokenizer(
490499
token.value,
491500
this.text,
@@ -696,13 +705,13 @@ export class Parser {
696705
(a) => a.key.name === "lang",
697706
)
698707
const lang = (langAttribute?.value as VLiteral)?.value
699-
if (
700-
lang &&
701-
lang !== "html" &&
702-
this.baseParserOptions.templateTokenizer?.[lang]
703-
) {
704-
this.processTemplateText(token, lang)
705-
return
708+
if (lang && lang !== "html") {
709+
const templateTokenizerOption =
710+
this.baseParserOptions.templateTokenizer?.[lang]
711+
if (templateTokenizerOption) {
712+
this.processTemplateText(token, templateTokenizerOption)
713+
return
714+
}
706715
}
707716
}
708717
parent.children.push({

Diff for: src/script/index.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import {
4242
analyzeExternalReferences,
4343
analyzeVariablesAndExternalReferences,
4444
} from "./scope-analyzer"
45-
import type { ESLintCustomParser } from "../common/espree"
4645
import {
4746
getEcmaVersionIfUseEspree,
4847
getEspreeFromUser,
@@ -60,6 +59,8 @@ import {
6059
} from "../script-setup/parser-options"
6160
import { isScriptSetupElement } from "../common/ast-utils"
6261
import type { LinesAndColumns } from "../common/lines-and-columns"
62+
import type { ParserObject } from "../common/parser-object"
63+
import { isEnhancedParserObject, isParserObject } from "../common/parser-object"
6364

6465
// [1] = aliases.
6566
// [2] = delimiter.
@@ -545,15 +546,16 @@ export function parseScript(
545546
code: string,
546547
parserOptions: ParserOptions,
547548
): ESLintExtendedProgram {
548-
const parser: ESLintCustomParser =
549+
const parser: ParserObject =
549550
typeof parserOptions.parser === "string"
550551
? loadParser(parserOptions.parser)
552+
: isParserObject(parserOptions.parser)
553+
? parserOptions.parser
551554
: getEspreeFromEcmaVersion(parserOptions.ecmaVersion)
552555

553-
const result: any =
554-
typeof parser.parseForESLint === "function"
555-
? parser.parseForESLint(code, parserOptions)
556-
: parser.parse(code, parserOptions)
556+
const result: any = isEnhancedParserObject(parser)
557+
? parser.parseForESLint(code, parserOptions)
558+
: parser.parse(code, parserOptions)
557559

558560
if (result.ast != null) {
559561
return result

Diff for: src/sfc/custom-block/index.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@ import { getEslintScope } from "../../common/eslint-scope"
1414
import { getEcmaVersionIfUseEspree } from "../../common/espree"
1515
import { fixErrorLocation, fixLocations } from "../../common/fix-locations"
1616
import type { LocationCalculatorForHtml } from "../../common/location-calculator"
17+
import type { ParserObject } from "../../common/parser-object"
18+
import { isEnhancedParserObject } from "../../common/parser-object"
1719
import type { ParserOptions } from "../../common/parser-options"
1820
import { DEFAULT_ECMA_VERSION } from "../../script-setup/parser-options"
1921

20-
export interface ESLintCustomBlockParser {
21-
parse(code: string, options: any): any
22-
parseForESLint?(code: string, options: any): any
23-
}
22+
export type ESLintCustomBlockParser = ParserObject<any, any>
2423

2524
export type CustomBlockContext = {
2625
getSourceCode(): SourceCode
@@ -181,10 +180,9 @@ function parseBlock(
181180
parser: ESLintCustomBlockParser,
182181
parserOptions: any,
183182
): any {
184-
const result: any =
185-
typeof parser.parseForESLint === "function"
186-
? parser.parseForESLint(code, parserOptions)
187-
: parser.parse(code, parserOptions)
183+
const result = isEnhancedParserObject(parser)
184+
? parser.parseForESLint(code, parserOptions)
185+
: parser.parse(code, parserOptions)
188186

189187
if (result.ast != null) {
190188
return result

Diff for: test/index.js

+40
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,46 @@ describe("Basic tests", () => {
308308
assert.deepStrictEqual(report[0].messages, [])
309309
assert.deepStrictEqual(report[1].messages, [])
310310
})
311+
312+
it("should notify no error with parser object with '@typescript-eslint/parser'", async () => {
313+
const cli = new ESLint({
314+
cwd: FIXTURE_DIR,
315+
overrideConfig: {
316+
env: { es6: true, node: true },
317+
parser: PARSER_PATH,
318+
parserOptions: {
319+
parser: require("@typescript-eslint/parser"),
320+
},
321+
rules: { semi: ["error", "never"] },
322+
},
323+
useEslintrc: false,
324+
})
325+
const report = await cli.lintFiles(["typed.js"])
326+
const messages = report[0].messages
327+
328+
assert.deepStrictEqual(messages, [])
329+
})
330+
331+
it("should notify no error with multiple parser object with '@typescript-eslint/parser'", async () => {
332+
const cli = new ESLint({
333+
cwd: FIXTURE_DIR,
334+
overrideConfig: {
335+
env: { es6: true, node: true },
336+
parser: PARSER_PATH,
337+
parserOptions: {
338+
parser: {
339+
ts: require("@typescript-eslint/parser"),
340+
},
341+
},
342+
rules: { semi: ["error", "never"] },
343+
},
344+
useEslintrc: false,
345+
})
346+
const report = await cli.lintFiles(["typed.ts", "typed.tsx"])
347+
348+
assert.deepStrictEqual(report[0].messages, [])
349+
assert.deepStrictEqual(report[1].messages, [])
350+
})
311351
}
312352
})
313353

0 commit comments

Comments
 (0)