Skip to content

feat(parser): always enable comment, loc, range, tokens #8617

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
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
90 changes: 50 additions & 40 deletions packages/parser/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { analyze } from '@typescript-eslint/scope-manager';
import type { Lib, TSESTree } from '@typescript-eslint/types';
import { ParserOptions } from '@typescript-eslint/types';
import type {
AST,
ParserServices,
TSESTreeOptions,
} from '@typescript-eslint/typescript-estree';
Expand All @@ -18,12 +19,14 @@ import { ScriptTarget } from 'typescript';

const log = debug('typescript-eslint:parser:parser');

interface ESLintProgram extends AST<{ comment: true; tokens: true }> {
comments: TSESTree.Comment[];
range: [number, number];
tokens: TSESTree.Token[];
}

interface ParseForESLintResult {
ast: TSESTree.Program & {
range?: [number, number];
tokens?: TSESTree.Token[];
comments?: TSESTree.Comment[];
};
ast: ESLintProgram;
services: ParserServices;
visitorKeys: VisitorKeys;
scopeManager: ScopeManager;
Expand Down Expand Up @@ -87,54 +90,59 @@ function parse(

function parseForESLint(
code: ts.SourceFile | string,
options?: ParserOptions | null,
parserOptions?: ParserOptions | null,
): ParseForESLintResult {
if (!options || typeof options !== 'object') {
options = {};
if (!parserOptions || typeof parserOptions !== 'object') {
parserOptions = {};
} else {
options = { ...options };
parserOptions = { ...parserOptions };
}
// https://eslint.org/docs/user-guide/configuring#specifying-parser-options
// if sourceType is not provided by default eslint expect that it will be set to "script"
if (options.sourceType !== 'module' && options.sourceType !== 'script') {
options.sourceType = 'script';
if (
parserOptions.sourceType !== 'module' &&
parserOptions.sourceType !== 'script'
) {
parserOptions.sourceType = 'script';
}
if (typeof options.ecmaFeatures !== 'object') {
options.ecmaFeatures = {};
if (typeof parserOptions.ecmaFeatures !== 'object') {
parserOptions.ecmaFeatures = {};
}

const parserOptions: TSESTreeOptions = {};
Object.assign(parserOptions, options, {
jsx: validateBoolean(options.ecmaFeatures.jsx),
/**
* Override errorOnTypeScriptSyntacticAndSemanticIssues and set it to false to prevent use from user config
* https://github.com/typescript-eslint/typescript-eslint/issues/8681#issuecomment-2000411834
*/
errorOnTypeScriptSyntacticAndSemanticIssues: false,
});
const analyzeOptions: AnalyzeOptions = {
globalReturn: options.ecmaFeatures.globalReturn,
jsxPragma: options.jsxPragma,
jsxFragmentName: options.jsxFragmentName,
lib: options.lib,
sourceType: options.sourceType,
};

/**
* Allow the user to suppress the warning from typescript-estree if they are using an unsupported
* version of TypeScript
*/
const warnOnUnsupportedTypeScriptVersion = validateBoolean(
options.warnOnUnsupportedTypeScriptVersion,
parserOptions.warnOnUnsupportedTypeScriptVersion,
true,
);

if (!warnOnUnsupportedTypeScriptVersion) {
parserOptions.loggerFn = false;
}
const tsestreeOptions = {
jsx: validateBoolean(parserOptions.ecmaFeatures.jsx),
...(!warnOnUnsupportedTypeScriptVersion && { loggerFn: false }),
...parserOptions,
// Override errorOnTypeScriptSyntacticAndSemanticIssues and set it to false to prevent use from user config
// https://github.com/typescript-eslint/typescript-eslint/issues/8681#issuecomment-2000411834
errorOnTypeScriptSyntacticAndSemanticIssues: false,
// comment, loc, range, and tokens should always be set for ESLint usage
// https://github.com/typescript-eslint/typescript-eslint/issues/8347
comment: true,
loc: true,
range: true,
tokens: true,
} satisfies TSESTreeOptions;

const analyzeOptions: AnalyzeOptions = {
globalReturn: parserOptions.ecmaFeatures.globalReturn,
jsxPragma: parserOptions.jsxPragma,
jsxFragmentName: parserOptions.jsxFragmentName,
lib: parserOptions.lib,
sourceType: parserOptions.sourceType,
};

const { ast, services } = parseAndGenerateServices(code, parserOptions);
ast.sourceType = options.sourceType;
const { ast, services } = parseAndGenerateServices(code, tsestreeOptions);
ast.sourceType = parserOptions.sourceType;

if (services.program) {
// automatically apply the options configured for the program
Expand Down Expand Up @@ -168,12 +176,14 @@ function parseForESLint(
}
}

// if not defined - override from the parserOptions
services.emitDecoratorMetadata ??= options.emitDecoratorMetadata === true;
services.experimentalDecorators ??= options.experimentalDecorators === true;

const scopeManager = analyze(ast, analyzeOptions);

// if not defined - override from the parserOptions
services.emitDecoratorMetadata ??=
parserOptions.emitDecoratorMetadata === true;
services.experimentalDecorators ??=
parserOptions.experimentalDecorators === true;

return { ast, services, scopeManager, visitorKeys };
}

Expand Down
144 changes: 105 additions & 39 deletions packages/parser/tests/lib/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as scopeManager from '@typescript-eslint/scope-manager';
import type { ParserOptions } from '@typescript-eslint/types';
import * as typescriptESTree from '@typescript-eslint/typescript-estree';
import path from 'path';
import { ScriptTarget } from 'typescript';

import { parse, parseForESLint } from '../../src/parser';

Expand All @@ -24,10 +25,6 @@ describe('parser', () => {
const code = 'const valid = true;';
const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices');
const config: ParserOptions = {
loc: false,
comment: false,
range: false,
tokens: false,
sourceType: 'module' as const,
ecmaFeatures: {
globalReturn: false,
Expand All @@ -43,71 +40,138 @@ describe('parser', () => {
parseForESLint(code, config);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenLastCalledWith(code, {
comment: true,
jsx: false,
loc: true,
range: true,
tokens: true,
...config,
});
});

it('parseAndGenerateServices() should be called with options.errorOnTypeScriptSyntacticAndSemanticIssues overriden to false', () => {
it('overrides `errorOnTypeScriptSyntacticAndSemanticIssues: false` when provided `errorOnTypeScriptSyntacticAndSemanticIssues: false`', () => {
const code = 'const valid = true;';
const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices');
const config: ParserOptions = {
loc: false,
comment: false,
range: false,
tokens: false,
sourceType: 'module' as const,
ecmaFeatures: {
globalReturn: false,
jsx: false,
},
// ts-estree specific
filePath: './isolated-file.src.ts',
project: 'tsconfig.json',
errorOnTypeScriptSyntacticAndSemanticIssues: true,
tsconfigRootDir: path.resolve(__dirname, '../fixtures/services'),
extraFileExtensions: ['.foo'],
};
parseForESLint(code, config);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenLastCalledWith(code, {
jsx: false,
...config,
parseForESLint(code, { errorOnTypeScriptSyntacticAndSemanticIssues: true });
expect(spy).toHaveBeenCalledWith(code, {
comment: true,
ecmaFeatures: {},
errorOnTypeScriptSyntacticAndSemanticIssues: false,
jsx: false,
loc: true,
range: true,
sourceType: 'script',
tokens: true,
});
});

it('`warnOnUnsupportedTypeScriptVersion: false` should set `loggerFn: false` on typescript-estree', () => {
it('sets `loggerFn: false` on typescript-estree when provided `warnOnUnsupportedTypeScriptVersion: false`', () => {
const code = 'const valid = true;';
const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices');
parseForESLint(code, { warnOnUnsupportedTypeScriptVersion: true });
parseForESLint(code, { warnOnUnsupportedTypeScriptVersion: false });
expect(spy).toHaveBeenCalledWith(code, {
comment: true,
ecmaFeatures: {},
errorOnTypeScriptSyntacticAndSemanticIssues: false,
jsx: false,
loc: true,
loggerFn: false,
range: true,
sourceType: 'script',
warnOnUnsupportedTypeScriptVersion: true,
tokens: true,
warnOnUnsupportedTypeScriptVersion: false,
});
spy.mockClear();
parseForESLint(code, { warnOnUnsupportedTypeScriptVersion: false });
});

it('sets `loggerFn: false` on typescript-estree when provided `warnOnUnsupportedTypeScriptVersion: true`', () => {
const code = 'const valid = true;';
const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices');
parseForESLint(code, { warnOnUnsupportedTypeScriptVersion: true });
expect(spy).toHaveBeenCalledWith(code, {
comment: true,
ecmaFeatures: {},
errorOnTypeScriptSyntacticAndSemanticIssues: false,
jsx: false,
loc: true,
range: true,
sourceType: 'script',
tokens: true,
warnOnUnsupportedTypeScriptVersion: true,
});
});

it('should call analyze() with inferred analyze options when no analyze options are provided', () => {
const code = 'const valid = true;';
const spy = jest.spyOn(scopeManager, 'analyze');
const config: ParserOptions = {
errorOnTypeScriptSyntacticAndSemanticIssues: false,
loggerFn: false,
warnOnUnsupportedTypeScriptVersion: false,
filePath: 'isolated-file.src.ts',
project: 'tsconfig.json',
tsconfigRootDir: path.join(__dirname, '../fixtures/services'),
};

parseForESLint(code, config);

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenLastCalledWith(expect.anything(), {
globalReturn: undefined,
jsxFragmentName: undefined,
jsxPragma: undefined,
lib: ['lib'],
sourceType: 'script',
});
});

it('analyze() should be called with options', () => {
it.each([
['esnext.full', ScriptTarget.ESNext],
['es2022.full', ScriptTarget.ES2022],
['es2021.full', ScriptTarget.ES2021],
['es2020.full', ScriptTarget.ES2020],
['es2019.full', ScriptTarget.ES2019],
['es2018.full', ScriptTarget.ES2018],
['es2017.full', ScriptTarget.ES2017],
['es2016.full', ScriptTarget.ES2016],
['es6', ScriptTarget.ES2015],
['lib', ScriptTarget.ES5],
['lib', undefined],
])(
'calls analyze() with `lib: [%s]` when the compiler options target is %s',
(lib, target) => {
const code = 'const valid = true;';
const spy = jest.spyOn(scopeManager, 'analyze');
const config: ParserOptions = {
filePath: 'isolated-file.src.ts',
project: 'tsconfig.json',
tsconfigRootDir: path.join(__dirname, '../fixtures/services'),
};

jest
.spyOn(typescriptESTree, 'parseAndGenerateServices')
.mockReturnValueOnce({
ast: {},
services: {
program: {
getCompilerOptions: () => ({ target }),
},
},
} as typescriptESTree.ParseAndGenerateServicesResult<typescriptESTree.TSESTreeOptions>);

parseForESLint(code, config);

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
lib: [lib],
}),
);
},
);

it('calls analyze() with the provided analyze options when analyze options are provided', () => {
const code = 'const valid = true;';
const spy = jest.spyOn(scopeManager, 'analyze');
const config: ParserOptions = {
loc: false,
comment: false,
range: false,
tokens: false,
sourceType: 'module' as const,
ecmaFeatures: {
globalReturn: false,
Expand All @@ -124,7 +188,9 @@ describe('parser', () => {
tsconfigRootDir: path.join(__dirname, '../fixtures/services'),
extraFileExtensions: ['.foo'],
};

parseForESLint(code, config);

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenLastCalledWith(expect.anything(), {
globalReturn: false,
Expand Down
4 changes: 1 addition & 3 deletions packages/types/src/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ interface ParserOptions {
experimentalDecorators?: boolean;

// typescript-estree specific
comment?: boolean;
debugLevel?: DebugLevel;
errorOnTypeScriptSyntacticAndSemanticIssues?: boolean;
errorOnUnknownASTType?: boolean;
Expand All @@ -67,10 +66,9 @@ interface ParserOptions {
extraFileExtensions?: string[];
filePath?: string;
jsDocParsingMode?: JSDocParsingMode;
loc?: boolean;
programs?: Program[] | null;
project?: string[] | string | boolean | null;
projectFolderIgnoreList?: (RegExp | string)[];
projectFolderIgnoreList?: string[];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't verified yet (typescript-eslint/examples#9), but it looks to me like we never actually supported the RegExp case?

const projectFolderIgnoreList = (
options.projectFolderIgnoreList ?? ['**/node_modules/**']
)
.reduce<string[]>((acc, folder) => {
if (typeof folder === 'string') {
acc.push(folder);
}
return acc;
}, [])

range?: boolean;
sourceType?: SourceType | undefined;
tokens?: boolean;
Expand Down
Loading
Loading