diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index 6aa369134a07..59ea3f2befe4 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -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'; @@ -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; @@ -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 @@ -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 }; } diff --git a/packages/parser/tests/lib/parser.test.ts b/packages/parser/tests/lib/parser.test.ts index cf373ebe2370..739558374f41 100644 --- a/packages/parser/tests/lib/parser.test.ts +++ b/packages/parser/tests/lib/parser.test.ts @@ -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'; @@ -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, @@ -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); + + 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, @@ -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, diff --git a/packages/types/src/parser-options.ts b/packages/types/src/parser-options.ts index e9600df308e5..44affc8e6c37 100644 --- a/packages/types/src/parser-options.ts +++ b/packages/types/src/parser-options.ts @@ -58,7 +58,6 @@ interface ParserOptions { experimentalDecorators?: boolean; // typescript-estree specific - comment?: boolean; debugLevel?: DebugLevel; errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; errorOnUnknownASTType?: boolean; @@ -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[]; range?: boolean; sourceType?: SourceType | undefined; tokens?: boolean; diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 210da9bcb042..b39a6e35c470 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -37,17 +37,17 @@ const JSDocParsingMode = { export function createParseSettings( code: ts.SourceFile | string, - options: Partial = {}, + tsestreeOptions: Partial = {}, ): MutableParseSettings { const codeFullText = enforceCodeString(code); - const singleRun = inferSingleRun(options); + const singleRun = inferSingleRun(tsestreeOptions); const tsconfigRootDir = - typeof options.tsconfigRootDir === 'string' - ? options.tsconfigRootDir + typeof tsestreeOptions.tsconfigRootDir === 'string' + ? tsestreeOptions.tsconfigRootDir : process.cwd(); - const passedLoggerFn = typeof options.loggerFn === 'function'; + const passedLoggerFn = typeof tsestreeOptions.loggerFn === 'function'; const jsDocParsingMode = ((): ts.JSDocParsingMode => { - switch (options.jsDocParsingMode) { + switch (tsestreeOptions.jsDocParsingMode) { case 'all': return JSDocParsingMode.ParseAll; @@ -63,64 +63,67 @@ export function createParseSettings( })(); const parseSettings: MutableParseSettings = { - allowInvalidAST: options.allowInvalidAST === true, + allowInvalidAST: tsestreeOptions.allowInvalidAST === true, code, codeFullText, - comment: options.comment === true, + comment: tsestreeOptions.comment === true, comments: [], debugLevel: - options.debugLevel === true + tsestreeOptions.debugLevel === true ? new Set(['typescript-eslint']) - : Array.isArray(options.debugLevel) - ? new Set(options.debugLevel) + : Array.isArray(tsestreeOptions.debugLevel) + ? new Set(tsestreeOptions.debugLevel) : new Set(), errorOnTypeScriptSyntacticAndSemanticIssues: false, - errorOnUnknownASTType: options.errorOnUnknownASTType === true, + errorOnUnknownASTType: tsestreeOptions.errorOnUnknownASTType === true, EXPERIMENTAL_projectService: - options.EXPERIMENTAL_useProjectService || - (options.project && - options.EXPERIMENTAL_useProjectService !== false && + tsestreeOptions.EXPERIMENTAL_useProjectService || + (tsestreeOptions.project && + tsestreeOptions.EXPERIMENTAL_useProjectService !== false && process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true') ? (TSSERVER_PROJECT_SERVICE ??= createProjectService( - options.EXPERIMENTAL_useProjectService, + tsestreeOptions.EXPERIMENTAL_useProjectService, jsDocParsingMode, )) : undefined, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: - options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === true, + tsestreeOptions.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === true, extraFileExtensions: - Array.isArray(options.extraFileExtensions) && - options.extraFileExtensions.every(ext => typeof ext === 'string') - ? options.extraFileExtensions + Array.isArray(tsestreeOptions.extraFileExtensions) && + tsestreeOptions.extraFileExtensions.every(ext => typeof ext === 'string') + ? tsestreeOptions.extraFileExtensions : [], filePath: ensureAbsolutePath( - typeof options.filePath === 'string' && options.filePath !== '' - ? options.filePath - : getFileName(options.jsx), + typeof tsestreeOptions.filePath === 'string' && + tsestreeOptions.filePath !== '' + ? tsestreeOptions.filePath + : getFileName(tsestreeOptions.jsx), tsconfigRootDir, ), jsDocParsingMode, - jsx: options.jsx === true, - loc: options.loc === true, + jsx: tsestreeOptions.jsx === true, + loc: tsestreeOptions.loc === true, log: - typeof options.loggerFn === 'function' - ? options.loggerFn - : options.loggerFn === false + typeof tsestreeOptions.loggerFn === 'function' + ? tsestreeOptions.loggerFn + : tsestreeOptions.loggerFn === false ? (): void => {} // eslint-disable-line @typescript-eslint/no-empty-function : console.log, // eslint-disable-line no-console - preserveNodeMaps: options.preserveNodeMaps !== false, - programs: Array.isArray(options.programs) ? options.programs : null, + preserveNodeMaps: tsestreeOptions.preserveNodeMaps !== false, + programs: Array.isArray(tsestreeOptions.programs) + ? tsestreeOptions.programs + : null, projects: [], - range: options.range === true, + range: tsestreeOptions.range === true, singleRun, suppressDeprecatedPropertyWarnings: - options.suppressDeprecatedPropertyWarnings ?? + tsestreeOptions.suppressDeprecatedPropertyWarnings ?? process.env.NODE_ENV !== 'test', - tokens: options.tokens === true ? [] : null, + tokens: tsestreeOptions.tokens === true ? [] : null, tsconfigMatchCache: (TSCONFIG_MATCH_CACHE ??= new ExpiringCache( singleRun ? 'Infinity' - : options.cacheLifetime?.glob ?? + : tsestreeOptions.cacheLifetime?.glob ?? DEFAULT_TSCONFIG_CACHE_DURATION_SECONDS, )), tsconfigRootDir, @@ -143,8 +146,8 @@ export function createParseSettings( debug.enable(namespaces.join(',')); } - if (Array.isArray(options.programs)) { - if (!options.programs.length) { + if (Array.isArray(tsestreeOptions.programs)) { + if (!tsestreeOptions.programs.length) { throw new Error( `You have set parserOptions.programs to an empty array. This will cause all files to not be found in existing programs. Either provide one or more existing TypeScript Program instances in the array, or remove the parserOptions.programs setting.`, ); @@ -157,9 +160,9 @@ export function createParseSettings( // Providing a program or project service overrides project resolution if (!parseSettings.programs && !parseSettings.EXPERIMENTAL_projectService) { parseSettings.projects = resolveProjectList({ - cacheLifetime: options.cacheLifetime, - project: getProjectConfigFiles(parseSettings, options.project), - projectFolderIgnoreList: options.projectFolderIgnoreList, + cacheLifetime: tsestreeOptions.cacheLifetime, + project: getProjectConfigFiles(parseSettings, tsestreeOptions.project), + projectFolderIgnoreList: tsestreeOptions.projectFolderIgnoreList, singleRun: parseSettings.singleRun, tsconfigRootDir: tsconfigRootDir, }); @@ -168,7 +171,7 @@ export function createParseSettings( // No type-aware linting which means that cross-file (or even same-file) JSDoc is useless // So in this specific case we default to 'none' if no value was provided if ( - options.jsDocParsingMode == null && + tsestreeOptions.jsDocParsingMode == null && parseSettings.projects.length === 0 && parseSettings.programs == null && parseSettings.EXPERIMENTAL_projectService == null diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 4c4b7f61a848..5ba1e64dad5a 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -81,11 +81,11 @@ function getProgramAndAST( ); } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface EmptyObject {} +/* eslint-disable @typescript-eslint/ban-types */ type AST = TSESTree.Program & - (T['comment'] extends true ? { comments: TSESTree.Comment[] } : EmptyObject) & - (T['tokens'] extends true ? { tokens: TSESTree.Token[] } : EmptyObject); + (T['comment'] extends true ? { comments: TSESTree.Comment[] } : {}) & + (T['tokens'] extends true ? { tokens: TSESTree.Token[] } : {}); +/* eslint-enable @typescript-eslint/ban-types */ interface ParseAndGenerateServicesResult { ast: AST; @@ -152,12 +152,12 @@ function clearParseAndGenerateServicesCalls(): void { function parseAndGenerateServices( code: ts.SourceFile | string, - options: T, + tsestreeOptions: T, ): ParseAndGenerateServicesResult { /** * Reset the parse configuration */ - const parseSettings = createParseSettings(code, options); + const parseSettings = createParseSettings(code, tsestreeOptions); /** * If this is a single run in which the user has not provided any existing programs but there @@ -196,8 +196,9 @@ function parseAndGenerateServices( parseSettings.programs != null || parseSettings.projects.length > 0; if ( - typeof options.errorOnTypeScriptSyntacticAndSemanticIssues === 'boolean' && - options.errorOnTypeScriptSyntacticAndSemanticIssues + typeof tsestreeOptions.errorOnTypeScriptSyntacticAndSemanticIssues === + 'boolean' && + tsestreeOptions.errorOnTypeScriptSyntacticAndSemanticIssues ) { parseSettings.errorOnTypeScriptSyntacticAndSemanticIssues = true; } @@ -219,15 +220,15 @@ function parseAndGenerateServices( * In this scenario we cannot rely upon the singleRun AOT compiled programs because the SourceFiles will not contain the source * with the latest fixes applied. Therefore we fallback to creating the quickest possible isolated program from the updated source. */ - if (parseSettings.singleRun && options.filePath) { - parseAndGenerateServicesCalls[options.filePath] = - (parseAndGenerateServicesCalls[options.filePath] || 0) + 1; + if (parseSettings.singleRun && tsestreeOptions.filePath) { + parseAndGenerateServicesCalls[tsestreeOptions.filePath] = + (parseAndGenerateServicesCalls[tsestreeOptions.filePath] || 0) + 1; } const { ast, program } = parseSettings.singleRun && - options.filePath && - parseAndGenerateServicesCalls[options.filePath] > 1 + tsestreeOptions.filePath && + parseAndGenerateServicesCalls[tsestreeOptions.filePath] > 1 ? createIsolatedProgram(parseSettings) : getProgramAndAST(parseSettings, hasFullTypeInformation);