From 3ead14ebfea65058f530c0870a6c362ac042b355 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 16 Oct 2022 04:57:51 +0200 Subject: [PATCH 1/4] chore: document and refactor 'extra' to 'parserSettings' --- .../typescript-estree/src/ast-converter.ts | 18 +- .../create-program/createDefaultProgram.ts | 35 +- .../create-program/createIsolatedProgram.ts | 22 +- .../create-program/createProjectProgram.ts | 30 +- .../src/create-program/createSourceFile.ts | 15 +- .../src/create-program/createWatchProgram.ts | 62 +-- .../src/create-program/shared.ts | 17 +- .../src/create-program/useProvidedPrograms.ts | 12 +- .../typescript-estree/src/inferSingleRun.ts | 46 ++ packages/typescript-estree/src/log.ts | 3 + .../typescript-estree/src/parseSettings.ts | 317 +++++++++++ .../typescript-estree/src/parser-options.ts | 28 - packages/typescript-estree/src/parser.ts | 491 ++---------------- .../src/warnAboutTSVersion.ts | 45 ++ .../src/components/linter/WebLinter.ts | 4 +- .../website/src/components/linter/config.ts | 7 +- 16 files changed, 569 insertions(+), 583 deletions(-) create mode 100644 packages/typescript-estree/src/inferSingleRun.ts create mode 100644 packages/typescript-estree/src/log.ts create mode 100644 packages/typescript-estree/src/parseSettings.ts create mode 100644 packages/typescript-estree/src/warnAboutTSVersion.ts diff --git a/packages/typescript-estree/src/ast-converter.ts b/packages/typescript-estree/src/ast-converter.ts index 86a1970aef7..b9be864f529 100644 --- a/packages/typescript-estree/src/ast-converter.ts +++ b/packages/typescript-estree/src/ast-converter.ts @@ -4,13 +4,13 @@ import type { ASTMaps } from './convert'; import { Converter, convertError } from './convert'; import { convertComments } from './convert-comments'; import { convertTokens } from './node-utils'; -import type { Extra } from './parser-options'; +import type { ParseSettings } from './parseSettings'; import { simpleTraverse } from './simple-traverse'; import type { TSESTree } from './ts-estree'; export function astConverter( ast: SourceFile, - extra: Extra, + parseSettings: ParseSettings, shouldPreserveNodeMaps: boolean, ): { estree: TSESTree.Program; astMaps: ASTMaps } { /** @@ -26,7 +26,7 @@ export function astConverter( * Recursively convert the TypeScript AST into an ESTree-compatible AST */ const instance = new Converter(ast, { - errorOnUnknownASTType: extra.errorOnUnknownASTType || false, + errorOnUnknownASTType: parseSettings.errorOnUnknownASTType || false, shouldPreserveNodeMaps, }); @@ -35,15 +35,15 @@ export function astConverter( /** * Optionally remove range and loc if specified */ - if (!extra.range || !extra.loc) { + if (!parseSettings.range || !parseSettings.loc) { simpleTraverse(estree, { enter: node => { - if (!extra.range) { + if (!parseSettings.range) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- TS 4.0 made this an error because the types aren't optional // @ts-expect-error delete node.range; } - if (!extra.loc) { + if (!parseSettings.loc) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- TS 4.0 made this an error because the types aren't optional // @ts-expect-error delete node.loc; @@ -55,15 +55,15 @@ export function astConverter( /** * Optionally convert and include all tokens in the AST */ - if (extra.tokens) { + if (parseSettings.tokens) { estree.tokens = convertTokens(ast); } /** * Optionally convert and include all comments in the AST */ - if (extra.comment) { - estree.comments = convertComments(ast, extra.code); + if (parseSettings.comment) { + estree.comments = convertComments(ast, parseSettings.code); } const astMaps = instance.getASTMaps(); diff --git a/packages/typescript-estree/src/create-program/createDefaultProgram.ts b/packages/typescript-estree/src/create-program/createDefaultProgram.ts index bebb194aef5..a2de81399d2 100644 --- a/packages/typescript-estree/src/create-program/createDefaultProgram.ts +++ b/packages/typescript-estree/src/create-program/createDefaultProgram.ts @@ -2,8 +2,8 @@ import debug from 'debug'; import path from 'path'; import * as ts from 'typescript'; -import type { Extra } from '../parser-options'; -import type { ASTAndProgram, CanonicalPath } from './shared'; +import type { ParseSettings } from '../parseSettings'; +import type { ASTAndProgram } from './shared'; import { createDefaultCompilerOptionsFromExtra, getModuleResolver, @@ -12,27 +12,26 @@ import { const log = debug('typescript-eslint:typescript-estree:createDefaultProgram'); /** - * @param code The code of the file being linted - * @param extra The config object - * @param extra.tsconfigRootDir The root directory for relative tsconfig paths - * @param extra.projects Provided tsconfig paths + * @param parseSettings Internal settings for parsing the file * @returns If found, returns the source file corresponding to the code and the containing program */ function createDefaultProgram( - code: string, - extra: Extra, + parseSettings: ParseSettings, ): ASTAndProgram | undefined { - log('Getting default program for: %s', extra.filePath || 'unnamed file'); + log( + 'Getting default program for: %s', + parseSettings.filePath || 'unnamed file', + ); - if (!extra.projects || extra.projects.length !== 1) { + if (parseSettings.projects?.length !== 1) { return undefined; } - const tsconfigPath: CanonicalPath = extra.projects[0]; + const tsconfigPath = parseSettings.projects[0]; const commandLine = ts.getParsedCommandLineOfConfigFile( tsconfigPath, - createDefaultCompilerOptionsFromExtra(extra), + createDefaultCompilerOptionsFromExtra(parseSettings), { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, ); @@ -45,24 +44,24 @@ function createDefaultProgram( /* setParentNodes */ true, ); - if (extra.moduleResolver) { + if (parseSettings.moduleResolver) { compilerHost.resolveModuleNames = getModuleResolver( - extra.moduleResolver, + parseSettings.moduleResolver, ).resolveModuleNames; } const oldReadFile = compilerHost.readFile; compilerHost.readFile = (fileName: string): string | undefined => - path.normalize(fileName) === path.normalize(extra.filePath) - ? code + path.normalize(fileName) === path.normalize(parseSettings.filePath) + ? parseSettings.code : oldReadFile(fileName); const program = ts.createProgram( - [extra.filePath], + [parseSettings.filePath], commandLine.options, compilerHost, ); - const ast = program.getSourceFile(extra.filePath); + const ast = program.getSourceFile(parseSettings.filePath); return ast && { ast, program }; } diff --git a/packages/typescript-estree/src/create-program/createIsolatedProgram.ts b/packages/typescript-estree/src/create-program/createIsolatedProgram.ts index ba19b843aeb..5ec1c8e0fe7 100644 --- a/packages/typescript-estree/src/create-program/createIsolatedProgram.ts +++ b/packages/typescript-estree/src/create-program/createIsolatedProgram.ts @@ -1,7 +1,7 @@ import debug from 'debug'; import * as ts from 'typescript'; -import type { Extra } from '../parser-options'; +import type { ParseSettings } from '../parseSettings'; import { getScriptKind } from './getScriptKind'; import type { ASTAndProgram } from './shared'; import { createDefaultCompilerOptionsFromExtra } from './shared'; @@ -12,11 +12,11 @@ const log = debug('typescript-eslint:typescript-estree:createIsolatedProgram'); * @param code The code of the file being linted * @returns Returns a new source file and program corresponding to the linted code */ -function createIsolatedProgram(code: string, extra: Extra): ASTAndProgram { +function createIsolatedProgram(parseSettings: ParseSettings): ASTAndProgram { log( 'Getting isolated program in %s mode for: %s', - extra.jsx ? 'TSX' : 'TS', - extra.filePath, + parseSettings.jsx ? 'TSX' : 'TS', + parseSettings.filePath, ); const compilerHost: ts.CompilerHost = { @@ -24,7 +24,7 @@ function createIsolatedProgram(code: string, extra: Extra): ASTAndProgram { return true; }, getCanonicalFileName() { - return extra.filePath; + return parseSettings.filePath; }, getCurrentDirectory() { return ''; @@ -43,10 +43,10 @@ function createIsolatedProgram(code: string, extra: Extra): ASTAndProgram { getSourceFile(filename: string) { return ts.createSourceFile( filename, - code, + parseSettings.code, ts.ScriptTarget.Latest, /* setParentNodes */ true, - getScriptKind(extra.filePath, extra.jsx), + getScriptKind(parseSettings.filePath, parseSettings.jsx), ); }, readFile() { @@ -61,17 +61,17 @@ function createIsolatedProgram(code: string, extra: Extra): ASTAndProgram { }; const program = ts.createProgram( - [extra.filePath], + [parseSettings.filePath], { noResolve: true, target: ts.ScriptTarget.Latest, - jsx: extra.jsx ? ts.JsxEmit.Preserve : undefined, - ...createDefaultCompilerOptionsFromExtra(extra), + jsx: parseSettings.jsx ? ts.JsxEmit.Preserve : undefined, + ...createDefaultCompilerOptionsFromExtra(parseSettings), }, compilerHost, ); - const ast = program.getSourceFile(extra.filePath); + const ast = program.getSourceFile(parseSettings.filePath); if (!ast) { throw new Error( 'Expected an ast to be returned for the single-file isolated program.', diff --git a/packages/typescript-estree/src/create-program/createProjectProgram.ts b/packages/typescript-estree/src/create-program/createProjectProgram.ts index 359c2b2dc90..3fe32c8052b 100644 --- a/packages/typescript-estree/src/create-program/createProjectProgram.ts +++ b/packages/typescript-estree/src/create-program/createProjectProgram.ts @@ -3,7 +3,7 @@ import path from 'path'; import * as ts from 'typescript'; import { firstDefined } from '../node-utils'; -import type { Extra } from '../parser-options'; +import type { ParseSettings } from '../parseSettings'; import { getProgramsForProjects } from './createWatchProgram'; import type { ASTAndProgram } from './shared'; import { getAstFromProgram } from './shared'; @@ -22,35 +22,33 @@ const DEFAULT_EXTRA_FILE_EXTENSIONS = [ ] as readonly string[]; /** - * @param code The code of the file being linted - * @param createDefaultProgram True if the default program should be created - * @param extra The config object - * @returns If found, returns the source file corresponding to the code and the containing program + * @param parseSettings.code Code of the file being parsed + * @param parseSettings.createDefaultProgram Whether the default program should be created + * @param parseSettings Internal settings for parsing the file + * @returns If found, the source file corresponding to the code and the containing program */ function createProjectProgram( - code: string, - createDefaultProgram: boolean, - extra: Extra, + parseSettings: ParseSettings, ): ASTAndProgram | undefined { - log('Creating project program for: %s', extra.filePath); + log('Creating project program for: %s', parseSettings.filePath); const astAndProgram = firstDefined( - getProgramsForProjects(code, extra.filePath, extra), - currentProgram => getAstFromProgram(currentProgram, extra), + getProgramsForProjects(parseSettings), + currentProgram => getAstFromProgram(currentProgram, parseSettings), ); - if (!astAndProgram && !createDefaultProgram) { + if (!astAndProgram && !parseSettings.createDefaultProgram) { // the file was either not matched within the tsconfig, or the extension wasn't expected const errorLines = [ '"parserOptions.project" has been set for @typescript-eslint/parser.', `The file does not match your project config: ${path.relative( - extra.tsconfigRootDir || process.cwd(), - extra.filePath, + parseSettings.tsconfigRootDir || process.cwd(), + parseSettings.filePath, )}.`, ]; let hasMatchedAnError = false; - const extraFileExtensions = extra.extraFileExtensions || []; + const extraFileExtensions = parseSettings.extraFileExtensions || []; extraFileExtensions.forEach(extraExtension => { if (!extraExtension.startsWith('.')) { @@ -65,7 +63,7 @@ function createProjectProgram( } }); - const fileExtension = path.extname(extra.filePath); + const fileExtension = path.extname(parseSettings.filePath); if (!DEFAULT_EXTRA_FILE_EXTENSIONS.includes(fileExtension)) { const nonStandardExt = `The extension for the file (${fileExtension}) is non-standard`; if (extraFileExtensions.length > 0) { diff --git a/packages/typescript-estree/src/create-program/createSourceFile.ts b/packages/typescript-estree/src/create-program/createSourceFile.ts index 107f027e46d..5d98266ad55 100644 --- a/packages/typescript-estree/src/create-program/createSourceFile.ts +++ b/packages/typescript-estree/src/create-program/createSourceFile.ts @@ -1,24 +1,27 @@ import debug from 'debug'; import * as ts from 'typescript'; -import type { Extra } from '../parser-options'; +import type { ParseSettings } from '../parseSettings'; import { getScriptKind } from './getScriptKind'; const log = debug('typescript-eslint:typescript-estree:createSourceFile'); -function createSourceFile(code: string, extra: Extra): ts.SourceFile { +function createSourceFile( + code: string, + parseSettings: ParseSettings, +): ts.SourceFile { log( 'Getting AST without type information in %s mode for: %s', - extra.jsx ? 'TSX' : 'TS', - extra.filePath, + parseSettings.jsx ? 'TSX' : 'TS', + parseSettings.filePath, ); return ts.createSourceFile( - extra.filePath, + parseSettings.filePath, code, ts.ScriptTarget.Latest, /* setParentNodes */ true, - getScriptKind(extra.filePath, extra.jsx), + getScriptKind(parseSettings.filePath, parseSettings.jsx), ); } diff --git a/packages/typescript-estree/src/create-program/createWatchProgram.ts b/packages/typescript-estree/src/create-program/createWatchProgram.ts index 1511023a97e..0e32f8ec1e5 100644 --- a/packages/typescript-estree/src/create-program/createWatchProgram.ts +++ b/packages/typescript-estree/src/create-program/createWatchProgram.ts @@ -3,7 +3,7 @@ import fs from 'fs'; import semver from 'semver'; import * as ts from 'typescript'; -import type { Extra } from '../parser-options'; +import type { ParseSettings } from '../parseSettings'; import type { CanonicalPath } from './shared'; import { canonicalDirname, @@ -121,40 +121,34 @@ function createHash(content: string): string { function updateCachedFileList( tsconfigPath: CanonicalPath, program: ts.Program, - extra: Extra, + parseSettings: ParseSettings, ): Set { - const fileList = extra.EXPERIMENTAL_useSourceOfProjectReferenceRedirect - ? new Set( - program.getSourceFiles().map(sf => getCanonicalFileName(sf.fileName)), - ) - : new Set(program.getRootFileNames().map(f => getCanonicalFileName(f))); + const fileList = + parseSettings.EXPERIMENTAL_useSourceOfProjectReferenceRedirect + ? new Set( + program.getSourceFiles().map(sf => getCanonicalFileName(sf.fileName)), + ) + : new Set(program.getRootFileNames().map(f => getCanonicalFileName(f))); programFileListCache.set(tsconfigPath, fileList); return fileList; } /** * Calculate project environments using options provided by consumer and paths from config - * @param code The code being linted - * @param filePathIn The path of the file being parsed - * @param extra.tsconfigRootDir The root directory for relative tsconfig paths - * @param extra.projects Provided tsconfig paths + * @param parseSettings Internal settings for parsing the file * @returns The programs corresponding to the supplied tsconfig paths */ -function getProgramsForProjects( - code: string, - filePathIn: string, - extra: Extra, -): ts.Program[] { - const filePath = getCanonicalFileName(filePathIn); +function getProgramsForProjects(parseSettings: ParseSettings): ts.Program[] { + const filePath = getCanonicalFileName(parseSettings.filePath); const results = []; // preserve reference to code and file being linted - currentLintOperationState.code = code; + currentLintOperationState.code = parseSettings.code; currentLintOperationState.filePath = filePath; // Update file version if necessary const fileWatchCallbacks = fileWatchCallbackTrackingMap.get(filePath); - const codeHash = createHash(code); + const codeHash = createHash(parseSettings.code); if ( parsedFilesSeenHash.get(filePath) !== codeHash && fileWatchCallbacks && @@ -174,7 +168,11 @@ function getProgramsForProjects( let updatedProgram: ts.Program | null = null; if (!fileList) { updatedProgram = existingWatch.getProgram().getProgram(); - fileList = updateCachedFileList(tsconfigPath, updatedProgram, extra); + fileList = updateCachedFileList( + tsconfigPath, + updatedProgram, + parseSettings, + ); } if (fileList.has(filePath)) { @@ -198,7 +196,7 @@ function getProgramsForProjects( * - the required program hasn't been created yet, or * - the file is new/renamed, and the program hasn't been updated. */ - for (const tsconfigPath of extra.projects) { + for (const tsconfigPath of parseSettings.projects) { const existingWatch = knownWatchProgramMap.get(tsconfigPath); if (existingWatch) { @@ -218,7 +216,7 @@ function getProgramsForProjects( const fileList = updateCachedFileList( tsconfigPath, updatedProgram, - extra, + parseSettings, ); if (fileList.has(filePath)) { log('Found updated program for file. %s', filePath); @@ -230,7 +228,7 @@ function getProgramsForProjects( continue; } - const programWatch = createWatchProgram(tsconfigPath, extra); + const programWatch = createWatchProgram(tsconfigPath, parseSettings); knownWatchProgramMap.set(tsconfigPath, programWatch); const program = programWatch.getProgram().getProgram(); @@ -238,7 +236,7 @@ function getProgramsForProjects( program.getTypeChecker(); // cache and check the file list - const fileList = updateCachedFileList(tsconfigPath, program, extra); + const fileList = updateCachedFileList(tsconfigPath, program, parseSettings); if (fileList.has(filePath)) { log('Found program for file. %s', filePath); // we can return early because we know this program contains the file @@ -257,23 +255,23 @@ const isRunningNoTimeoutFix = semver.satisfies(ts.version, '>=3.9.0-beta', { function createWatchProgram( tsconfigPath: string, - extra: Extra, + parseSettings: ParseSettings, ): ts.WatchOfConfigFile { log('Creating watch program for %s.', tsconfigPath); // create compiler host const watchCompilerHost = ts.createWatchCompilerHost( tsconfigPath, - createDefaultCompilerOptionsFromExtra(extra), + createDefaultCompilerOptionsFromExtra(parseSettings), ts.sys, ts.createAbstractBuilder, diagnosticReporter, /*reportWatchStatus*/ () => {}, ) as WatchCompilerHostOfConfigFile; - if (extra.moduleResolver) { + if (parseSettings.moduleResolver) { watchCompilerHost.resolveModuleNames = getModuleResolver( - extra.moduleResolver, + parseSettings.moduleResolver, ).resolveModuleNames; } @@ -337,7 +335,9 @@ function createWatchProgram( ): string[] => oldReadDirectory( path, - !extensions ? undefined : extensions.concat(extra.extraFileExtensions), + !extensions + ? undefined + : extensions.concat(parseSettings.extraFileExtensions), exclude, include, depth, @@ -345,7 +345,7 @@ function createWatchProgram( oldOnDirectoryStructureHostCreate(host); }; // This works only on 3.9 - watchCompilerHost.extraFileExtensions = extra.extraFileExtensions.map( + watchCompilerHost.extraFileExtensions = parseSettings.extraFileExtensions.map( extension => ({ extension, isMixedContent: true, @@ -359,7 +359,7 @@ function createWatchProgram( * See https://github.com/typescript-eslint/typescript-eslint/issues/2094 */ watchCompilerHost.useSourceOfProjectReferenceRedirect = (): boolean => - extra.EXPERIMENTAL_useSourceOfProjectReferenceRedirect; + parseSettings.EXPERIMENTAL_useSourceOfProjectReferenceRedirect; // Since we don't want to asynchronously update program we want to disable timeout methods // So any changes in the program will be delayed and updated when getProgram is called on watch diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index 63ba34a5f83..dd50f757dce 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -2,7 +2,8 @@ import path from 'path'; import type { Program } from 'typescript'; import * as ts from 'typescript'; -import type { Extra, ModuleResolver } from '../parser-options'; +import type { ModuleResolver } from '../parser-options'; +import type { ParseSettings } from '../parseSettings'; interface ASTAndProgram { ast: ts.SourceFile; @@ -33,9 +34,9 @@ const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { }; function createDefaultCompilerOptionsFromExtra( - extra: Extra, + parseSettings: ParseSettings, ): ts.CompilerOptions { - if (extra.debugLevel.has('typescript')) { + if (parseSettings.debugLevel.has('typescript')) { return { ...DEFAULT_COMPILER_OPTIONS, extendedDiagnostics: true, @@ -63,10 +64,10 @@ function getCanonicalFileName(filePath: string): CanonicalPath { return correctPathCasing(normalized) as CanonicalPath; } -function ensureAbsolutePath(p: string, extra: Extra): string { +function ensureAbsolutePath(p: string, tsconfigRootDir: string): string { return path.isAbsolute(p) ? p - : path.join(extra.tsconfigRootDir || process.cwd(), p); + : path.join(tsconfigRootDir || process.cwd(), p); } function canonicalDirname(p: CanonicalPath): CanonicalPath { @@ -92,12 +93,12 @@ function getExtension(fileName: string | undefined): string | null { function getAstFromProgram( currentProgram: Program, - extra: Extra, + parseSettings: ParseSettings, ): ASTAndProgram | undefined { - const ast = currentProgram.getSourceFile(extra.filePath); + const ast = currentProgram.getSourceFile(parseSettings.filePath); // working around https://github.com/typescript-eslint/typescript-eslint/issues/1573 - const expectedExt = getExtension(extra.filePath); + const expectedExt = getExtension(parseSettings.filePath); const returnedExt = getExtension(ast?.fileName); if (expectedExt !== returnedExt) { return undefined; diff --git a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts index 0a8300f5ad2..fc99416faa5 100644 --- a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts +++ b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import type { Extra } from '../parser-options'; +import type { ParseSettings } from '../parseSettings'; import type { ASTAndProgram } from './shared'; import { CORE_COMPILER_OPTIONS, getAstFromProgram } from './shared'; @@ -11,16 +11,16 @@ const log = debug('typescript-eslint:typescript-estree:useProvidedProgram'); function useProvidedPrograms( programInstances: Iterable, - extra: Extra, + parseSettings: ParseSettings, ): ASTAndProgram | undefined { log( 'Retrieving ast for %s from provided program instance(s)', - extra.filePath, + parseSettings.filePath, ); let astAndProgram: ASTAndProgram | undefined; for (const programInstance of programInstances) { - astAndProgram = getAstFromProgram(programInstance, extra); + astAndProgram = getAstFromProgram(programInstance, parseSettings); // Stop at the first applicable program instance if (astAndProgram) { break; @@ -29,8 +29,8 @@ function useProvidedPrograms( if (!astAndProgram) { const relativeFilePath = path.relative( - extra.tsconfigRootDir || process.cwd(), - extra.filePath, + parseSettings.tsconfigRootDir || process.cwd(), + parseSettings.filePath, ); const errorLines = [ '"parserOptions.programs" has been provided for @typescript-eslint/parser.', diff --git a/packages/typescript-estree/src/inferSingleRun.ts b/packages/typescript-estree/src/inferSingleRun.ts new file mode 100644 index 00000000000..27f918d42fe --- /dev/null +++ b/packages/typescript-estree/src/inferSingleRun.ts @@ -0,0 +1,46 @@ +import { normalize } from 'path'; + +import type { TSESTreeOptions } from './parser-options'; + +/** + * ESLint (and therefore typescript-eslint) is used in both "single run"/one-time contexts, + * such as an ESLint CLI invocation, and long-running sessions (such as continuous feedback + * on a file in an IDE). + * + * When typescript-eslint handles TypeScript Program management behind the scenes, this distinction + * is important because there is significant overhead to managing the so called Watch Programs + * needed for the long-running use-case. We therefore use the following logic to figure out which + * of these contexts applies to the current execution. + * + * @returns Whether this is part of a single run, rather than a long-running process. + */ +export function inferSingleRun(options: TSESTreeOptions | undefined): boolean { + // Allow users to explicitly inform us of their intent to perform a single run (or not) with TSESTREE_SINGLE_RUN + if (process.env.TSESTREE_SINGLE_RUN === 'false') { + return false; + } + if (process.env.TSESTREE_SINGLE_RUN === 'true') { + return true; + } + + // Currently behind a flag while we gather real-world feedback + if (options?.allowAutomaticSingleRunInference) { + if ( + // Default to single runs for CI processes. CI=true is set by most CI providers by default. + process.env.CI === 'true' || + // This will be true for invocations such as `npx eslint ...` and `./node_modules/.bin/eslint ...` + process.argv[1].endsWith(normalize('node_modules/.bin/eslint')) + ) { + return true; + } + } + + /** + * We default to assuming that this run could be part of a long-running session (e.g. in an IDE) + * and watch programs will therefore be required. + * + * Unless we can reliably infer otherwise, we default to assuming that this run could be part + * of a long-running session (e.g. in an IDE) and watch programs will therefore be required + */ + return false; +} diff --git a/packages/typescript-estree/src/log.ts b/packages/typescript-estree/src/log.ts new file mode 100644 index 00000000000..6bb628bf1ce --- /dev/null +++ b/packages/typescript-estree/src/log.ts @@ -0,0 +1,3 @@ +import debug from 'debug'; + +export const log = debug('typescript-eslint:typescript-estree:parser'); diff --git a/packages/typescript-estree/src/parseSettings.ts b/packages/typescript-estree/src/parseSettings.ts new file mode 100644 index 00000000000..f24ae7cf5b1 --- /dev/null +++ b/packages/typescript-estree/src/parseSettings.ts @@ -0,0 +1,317 @@ +import debug from 'debug'; +import { sync as globSync } from 'globby'; +import isGlob from 'is-glob'; +import type * as ts from 'typescript'; + +import type { CanonicalPath } from './create-program/shared'; +import { + ensureAbsolutePath, + getCanonicalFileName, +} from './create-program/shared'; +import { inferSingleRun } from './inferSingleRun'; +import { log } from './log'; +import type { TSESTreeOptions } from './parser-options'; +import type { TSESTree } from './ts-estree'; +import { warnAboutTSVersion } from './warnAboutTSVersion'; + +type DebugModule = 'typescript-eslint' | 'eslint' | 'typescript'; + +/** + * Internal settings used by the parser to run on a file. + */ +export interface ParseSettings { + /** + * Code of the file being parsed. + */ + code: string; + + /** + * Whether the `comment` parse option is enabled. + */ + comment: boolean; + + /** + * If the `comment` parse option is enabled, retrieved comments. + */ + comments: TSESTree.Comment[]; + + /** + * Whether to create a TypeScript program if one is not provided. + */ + createDefaultProgram: boolean; + + /** + * Which debug areas should be logged. + */ + debugLevel: Set; + + /** + * Whether to error if TypeScript reports a semantic or syntactic error diagnostic. + */ + errorOnTypeScriptSyntacticAndSemanticIssues: boolean; + + /** + * Whether to error if an unknown AST node type is encountered. + */ + errorOnUnknownASTType: boolean; + + /** + * Whether TS should use the source files for referenced projects instead of the compiled .d.ts files. + * + * @remarks + * This feature is not yet optimized, and is likely to cause OOMs for medium to large projects. + * This flag REQUIRES at least TS v3.9, otherwise it does nothing. + */ + EXPERIMENTAL_useSourceOfProjectReferenceRedirect: boolean; + + /** + * Any non-standard file extensions which will be parsed. + */ + extraFileExtensions: string[]; + + /** + * Path of the file being parsed. + */ + filePath: string; + + /** + * Whether parsing of JSX is enabled. + * + * @remarks The applicable file extension is still required. + */ + jsx: boolean; + + /** + * Whether to add `loc` information to each node. + */ + loc: boolean; + + /** + * Log function, if not `console.log`. + */ + log: (message: string) => void; + + /** + * Path for a module resolver to use for the compiler host's `resolveModuleNames`. + */ + moduleResolver: string; + + /** + * Whether two-way AST node maps are preserved during the AST conversion process. + */ + preserveNodeMaps?: boolean; + + /** + * One or more instances of TypeScript Program objects to be used for type information. + */ + programs: null | Iterable; + + /** + * Normalized paths to provided project paths. + */ + projects: CanonicalPath[]; + + /** + * Whether to add the `range` property to AST nodes. + */ + range: boolean; + + /** + * Whether this is part of a single run, rather than a long-running process. + */ + singleRun: boolean; + + /** + * If the `cotokensmment` parse option is enabled, retrieved tokens. + */ + tokens: null | TSESTree.Token[]; + + /** + * The absolute path to the root directory for all provided `project`s. + */ + tsconfigRootDir: string; +} + +export function createParseSettings( + code: string, + options: Partial = {}, +): ParseSettings { + const tsconfigRootDir = + typeof options.tsconfigRootDir === 'string' + ? options.tsconfigRootDir + : process.cwd(); + const parseSettings: ParseSettings = { + code: enforceString(code), + comment: options.comment === true, + comments: [], + createDefaultProgram: options.createDefaultProgram === true, + debugLevel: + options.debugLevel === true + ? new Set(['typescript-eslint']) + : Array.isArray(options.debugLevel) + ? new Set(options.debugLevel) + : new Set(), + errorOnTypeScriptSyntacticAndSemanticIssues: false, + errorOnUnknownASTType: options.errorOnUnknownASTType === true, + EXPERIMENTAL_useSourceOfProjectReferenceRedirect: + options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === true, + extraFileExtensions: + Array.isArray(options.extraFileExtensions) && + options.extraFileExtensions.every(ext => typeof ext === 'string') + ? options.extraFileExtensions + : [], + filePath: ensureAbsolutePath( + typeof options.filePath === 'string' && options.filePath !== '' + ? options.filePath + : getFileName(options.jsx), + tsconfigRootDir, + ), + jsx: options.jsx === true, + loc: options.loc === true, + log: + typeof options.loggerFn === 'function' + ? options.loggerFn + : options.loggerFn === false + ? (): void => {} + : console.log, // eslint-disable-line no-console + moduleResolver: options.moduleResolver ?? '', + preserveNodeMaps: options.preserveNodeMaps !== false, + programs: Array.isArray(options.programs) ? options.programs : null, + projects: [], + range: options.range === true, + singleRun: inferSingleRun(options), + tokens: options.tokens === true ? [] : null, + tsconfigRootDir, + }; + + // debug doesn't support multiple `enable` calls, so have to do it all at once + if (parseSettings.debugLevel.size > 0) { + const namespaces = []; + if (parseSettings.debugLevel.has('typescript-eslint')) { + namespaces.push('typescript-eslint:*'); + } + if ( + parseSettings.debugLevel.has('eslint') || + // make sure we don't turn off the eslint debug if it was enabled via --debug + debug.enabled('eslint:*,-eslint:code-path') + ) { + // https://github.com/eslint/eslint/blob/9dfc8501fb1956c90dc11e6377b4cb38a6bea65d/bin/eslint.js#L25 + namespaces.push('eslint:*,-eslint:code-path'); + } + debug.enable(namespaces.join(',')); + } + + if (Array.isArray(options.programs)) { + if (!options.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.`, + ); + } + log( + 'parserOptions.programs was provided, so parserOptions.project will be ignored.', + ); + } + + // Providing a program overrides project resolution + if (!parseSettings.programs) { + const projectFolderIgnoreList = ( + options.projectFolderIgnoreList ?? ['**/node_modules/**'] + ) + .reduce((acc, folder) => { + if (typeof folder === 'string') { + acc.push(folder); + } + return acc; + }, []) + // prefix with a ! for not match glob + .map(folder => (folder.startsWith('!') ? folder : `!${folder}`)); + + parseSettings.projects = prepareAndTransformProjects( + tsconfigRootDir, + options.project, + projectFolderIgnoreList, + ); + } + + warnAboutTSVersion(parseSettings); + + return parseSettings; +} + +/** + * Ensures source code is a string. + */ +function enforceString(code: unknown): string { + if (typeof code !== 'string') { + return String(code); + } + + return code; +} + +/** + * Compute the filename based on the parser options. + * + * Even if jsx option is set in typescript compiler, filename still has to + * contain .tsx file extension. + * + * @param options Parser options + */ +function getFileName(jsx?: boolean): string { + return jsx ? 'estree.tsx' : 'estree.ts'; +} + +function getTsconfigPath( + tsconfigPath: string, + tsconfigRootDir: string, +): CanonicalPath { + return getCanonicalFileName( + ensureAbsolutePath(tsconfigPath, tsconfigRootDir), + ); +} + +/** + * Normalizes, sanitizes, resolves and filters the provided project paths + */ +function prepareAndTransformProjects( + tsconfigRootDir: string, + projectsInput: string | string[] | undefined, + ignoreListInput: string[], +): CanonicalPath[] { + const sanitizedProjects: string[] = []; + + // Normalize and sanitize the project paths + if (typeof projectsInput === 'string') { + sanitizedProjects.push(projectsInput); + } else if (Array.isArray(projectsInput)) { + for (const project of projectsInput) { + if (typeof project === 'string') { + sanitizedProjects.push(project); + } + } + } + + if (sanitizedProjects.length === 0) { + return []; + } + + // Transform glob patterns into paths + const nonGlobProjects = sanitizedProjects.filter(project => !isGlob(project)); + const globProjects = sanitizedProjects.filter(project => isGlob(project)); + const uniqueCanonicalProjectPaths = new Set( + nonGlobProjects + .concat( + globSync([...globProjects, ...ignoreListInput], { + cwd: tsconfigRootDir, + }), + ) + .map(project => getTsconfigPath(project, tsconfigRootDir)), + ); + + log( + 'parserOptions.project (excluding ignored) matched projects: %s', + uniqueCanonicalProjectPaths, + ); + + return Array.from(uniqueCanonicalProjectPaths); +} diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index cfe82b15f22..cec95c3b413 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -1,36 +1,8 @@ import type { DebugLevel } from '@typescript-eslint/types'; import type * as ts from 'typescript'; -import type { CanonicalPath } from './create-program/shared'; import type { TSESTree, TSESTreeToTSNode, TSNode, TSToken } from './ts-estree'; -type DebugModule = 'typescript-eslint' | 'eslint' | 'typescript'; - -export interface Extra { - code: string; - comment: boolean; - comments: TSESTree.Comment[]; - createDefaultProgram: boolean; - debugLevel: Set; - errorOnTypeScriptSyntacticAndSemanticIssues: boolean; - errorOnUnknownASTType: boolean; - EXPERIMENTAL_useSourceOfProjectReferenceRedirect: boolean; - extraFileExtensions: string[]; - filePath: string; - jsx: boolean; - loc: boolean; - singleRun: boolean; - log: (message: string) => void; - preserveNodeMaps?: boolean; - programs: null | Iterable; - projects: CanonicalPath[]; - range: boolean; - strict: boolean; - tokens: null | TSESTree.Token[]; - tsconfigRootDir: string; - moduleResolver: string; -} - //////////////////////////////////////////////////// // MAKE SURE THIS IS KEPT IN SYNC WITH THE README // //////////////////////////////////////////////////// diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index ae461a24dd5..a00caeee89c 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -1,9 +1,4 @@ -import debug from 'debug'; -import { sync as globSync } from 'globby'; -import isGlob from 'is-glob'; -import { normalize } from 'path'; -import semver from 'semver'; -import * as ts from 'typescript'; +import type * as ts from 'typescript'; import { astConverter } from './ast-converter'; import { convertError } from './convert'; @@ -12,41 +7,17 @@ import { createIsolatedProgram } from './create-program/createIsolatedProgram'; import { createProjectProgram } from './create-program/createProjectProgram'; import { createSourceFile } from './create-program/createSourceFile'; import type { ASTAndProgram, CanonicalPath } from './create-program/shared'; -import { - ensureAbsolutePath, - getCanonicalFileName, -} from './create-program/shared'; import { createProgramFromConfigFile, useProvidedPrograms, } from './create-program/useProvidedPrograms'; -import type { Extra, ParserServices, TSESTreeOptions } from './parser-options'; +import { log } from './log'; +import type { ParserServices, TSESTreeOptions } from './parser-options'; +import type { ParseSettings } from './parseSettings'; +import { createParseSettings } from './parseSettings'; import { getFirstSemanticOrSyntacticError } from './semantic-or-syntactic-errors'; import type { TSESTree } from './ts-estree'; -const log = debug('typescript-eslint:typescript-estree:parser'); - -/** - * This needs to be kept in sync with the top-level README.md in the - * typescript-eslint monorepo - */ -const SUPPORTED_TYPESCRIPT_VERSIONS = '>=3.3.1 <4.9.0'; -/* - * The semver package will ignore prerelease ranges, and we don't want to explicitly document every one - * List them all separately here, so we can automatically create the full string - */ -const SUPPORTED_PRERELEASE_RANGES: string[] = []; -const ACTIVE_TYPESCRIPT_VERSION = ts.version; -const isRunningSupportedTypeScriptVersion = semver.satisfies( - ACTIVE_TYPESCRIPT_VERSION, - [SUPPORTED_TYPESCRIPT_VERSIONS] - .concat(SUPPORTED_PRERELEASE_RANGES) - .join(' || '), -); - -let extra: Extra; -let warnedAboutTSVersion = false; - /** * Cache existing programs for the single run use-case. * @@ -57,350 +28,26 @@ function clearProgramCache(): void { existingPrograms.clear(); } -function enforceString(code: unknown): string { - /** - * Ensure the source code is a string - */ - if (typeof code !== 'string') { - return String(code); - } - - return code; -} - /** - * @param code The code of the file being linted - * @param programInstances One or more (potentially lazily constructed) existing programs to use + * @param parseSettings Internal settings for parsing the file * @param shouldProvideParserServices True if the program should be attempted to be calculated from provided tsconfig files - * @param shouldCreateDefaultProgram True if the program should be created from compiler host * @returns Returns a source file and program corresponding to the linted code */ function getProgramAndAST( - code: string, - programInstances: Iterable | null, + parseSettings: ParseSettings, shouldProvideParserServices: boolean, - shouldCreateDefaultProgram: boolean, ): ASTAndProgram { return ( - (programInstances && useProvidedPrograms(programInstances, extra)) || - (shouldProvideParserServices && - createProjectProgram(code, shouldCreateDefaultProgram, extra)) || + (parseSettings.programs && + useProvidedPrograms(parseSettings.programs, parseSettings)) || + (shouldProvideParserServices && createProjectProgram(parseSettings)) || (shouldProvideParserServices && - shouldCreateDefaultProgram && - createDefaultProgram(code, extra)) || - createIsolatedProgram(code, extra) + parseSettings.createDefaultProgram && + createDefaultProgram(parseSettings)) || + createIsolatedProgram(parseSettings) ); } -/** - * Compute the filename based on the parser options. - * - * Even if jsx option is set in typescript compiler, filename still has to - * contain .tsx file extension. - * - * @param options Parser options - */ -function getFileName({ jsx }: { jsx?: boolean } = {}): string { - return jsx ? 'estree.tsx' : 'estree.ts'; -} - -/** - * Resets the extra config object - */ -function resetExtra(): void { - extra = { - code: '', - comment: false, - comments: [], - createDefaultProgram: false, - debugLevel: new Set(), - errorOnTypeScriptSyntacticAndSemanticIssues: false, - errorOnUnknownASTType: false, - EXPERIMENTAL_useSourceOfProjectReferenceRedirect: false, - extraFileExtensions: [], - filePath: getFileName(), - jsx: false, - loc: false, - log: console.log, // eslint-disable-line no-console - preserveNodeMaps: true, - programs: null, - projects: [], - range: false, - strict: false, - tokens: null, - tsconfigRootDir: process.cwd(), - /** - * Unless we can reliably infer otherwise, we default to assuming that this run could be part - * of a long-running session (e.g. in an IDE) and watch programs will therefore be required - */ - singleRun: false, - moduleResolver: '', - }; -} - -function getTsconfigPath(tsconfigPath: string, extra: Extra): CanonicalPath { - return getCanonicalFileName(ensureAbsolutePath(tsconfigPath, extra)); -} - -/** - * Normalizes, sanitizes, resolves and filters the provided project paths - */ -function prepareAndTransformProjects( - projectsInput: string | string[] | undefined, - ignoreListInput: string[], -): CanonicalPath[] { - const sanitizedProjects: string[] = []; - - // Normalize and sanitize the project paths - if (typeof projectsInput === 'string') { - sanitizedProjects.push(projectsInput); - } else if (Array.isArray(projectsInput)) { - for (const project of projectsInput) { - if (typeof project === 'string') { - sanitizedProjects.push(project); - } - } - } - - if (sanitizedProjects.length === 0) { - return []; - } - - // Transform glob patterns into paths - const nonGlobProjects = sanitizedProjects.filter(project => !isGlob(project)); - const globProjects = sanitizedProjects.filter(project => isGlob(project)); - const uniqueCanonicalProjectPaths = new Set( - nonGlobProjects - .concat( - globSync([...globProjects, ...ignoreListInput], { - cwd: extra.tsconfigRootDir, - }), - ) - .map(project => getTsconfigPath(project, extra)), - ); - - log( - 'parserOptions.project (excluding ignored) matched projects: %s', - uniqueCanonicalProjectPaths, - ); - - return Array.from(uniqueCanonicalProjectPaths); -} - -function applyParserOptionsToExtra(options: TSESTreeOptions): void { - /** - * Configure Debug logging - */ - if (options.debugLevel === true) { - extra.debugLevel = new Set(['typescript-eslint']); - } else if (Array.isArray(options.debugLevel)) { - extra.debugLevel = new Set(options.debugLevel); - } - if (extra.debugLevel.size > 0) { - // debug doesn't support multiple `enable` calls, so have to do it all at once - const namespaces = []; - if (extra.debugLevel.has('typescript-eslint')) { - namespaces.push('typescript-eslint:*'); - } - if ( - extra.debugLevel.has('eslint') || - // make sure we don't turn off the eslint debug if it was enabled via --debug - debug.enabled('eslint:*,-eslint:code-path') - ) { - // https://github.com/eslint/eslint/blob/9dfc8501fb1956c90dc11e6377b4cb38a6bea65d/bin/eslint.js#L25 - namespaces.push('eslint:*,-eslint:code-path'); - } - debug.enable(namespaces.join(',')); - } - - /** - * Track range information in the AST - */ - extra.range = typeof options.range === 'boolean' && options.range; - extra.loc = typeof options.loc === 'boolean' && options.loc; - - /** - * Track tokens in the AST - */ - if (typeof options.tokens === 'boolean' && options.tokens) { - extra.tokens = []; - } - - /** - * Track comments in the AST - */ - if (typeof options.comment === 'boolean' && options.comment) { - extra.comment = true; - extra.comments = []; - } - - /** - * Enable JSX - note the applicable file extension is still required - */ - if (typeof options.jsx !== 'boolean') { - extra.jsx = false; - } else { - extra.jsx = options.jsx; - } - - /** - * Get the file path - */ - if (typeof options.filePath === 'string' && options.filePath !== '') { - extra.filePath = options.filePath; - } else { - extra.filePath = getFileName(extra); - } - - /** - * Allow the user to cause the parser to error if it encounters an unknown AST Node Type - * (used in testing) - */ - if ( - typeof options.errorOnUnknownASTType === 'boolean' && - options.errorOnUnknownASTType - ) { - extra.errorOnUnknownASTType = true; - } - - /** - * Allow the user to override the function used for logging - */ - if (typeof options.loggerFn === 'function') { - extra.log = options.loggerFn; - } else if (options.loggerFn === false) { - extra.log = (): void => {}; - } - - if (typeof options.tsconfigRootDir === 'string') { - extra.tsconfigRootDir = options.tsconfigRootDir; - } - - // NOTE - ensureAbsolutePath relies upon having the correct tsconfigRootDir in extra - extra.filePath = ensureAbsolutePath(extra.filePath, extra); - - if (Array.isArray(options.programs)) { - if (!options.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.`, - ); - } - extra.programs = options.programs; - log( - 'parserOptions.programs was provided, so parserOptions.project will be ignored.', - ); - } - - if (!extra.programs) { - // providing a program overrides project resolution - const projectFolderIgnoreList = ( - options.projectFolderIgnoreList ?? ['**/node_modules/**'] - ) - .reduce((acc, folder) => { - if (typeof folder === 'string') { - acc.push(folder); - } - return acc; - }, []) - // prefix with a ! for not match glob - .map(folder => (folder.startsWith('!') ? folder : `!${folder}`)); - // NOTE - prepareAndTransformProjects relies upon having the correct tsconfigRootDir in extra - extra.projects = prepareAndTransformProjects( - options.project, - projectFolderIgnoreList, - ); - } - - if ( - Array.isArray(options.extraFileExtensions) && - options.extraFileExtensions.every(ext => typeof ext === 'string') - ) { - extra.extraFileExtensions = options.extraFileExtensions; - } - - /** - * Allow the user to enable or disable the preservation of the AST node maps - * during the conversion process. - */ - if (typeof options.preserveNodeMaps === 'boolean') { - extra.preserveNodeMaps = options.preserveNodeMaps; - } - - extra.createDefaultProgram = - typeof options.createDefaultProgram === 'boolean' && - options.createDefaultProgram; - - extra.EXPERIMENTAL_useSourceOfProjectReferenceRedirect = - typeof options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === - 'boolean' && options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect; - - if (typeof options.moduleResolver === 'string') { - extra.moduleResolver = options.moduleResolver; - } -} - -function warnAboutTSVersion(): void { - if (!isRunningSupportedTypeScriptVersion && !warnedAboutTSVersion) { - const isTTY = - typeof process === 'undefined' ? false : process.stdout?.isTTY; - if (isTTY) { - const border = '============='; - const versionWarning = [ - border, - 'WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.', - 'You may find that it works just fine, or you may not.', - `SUPPORTED TYPESCRIPT VERSIONS: ${SUPPORTED_TYPESCRIPT_VERSIONS}`, - `YOUR TYPESCRIPT VERSION: ${ACTIVE_TYPESCRIPT_VERSION}`, - 'Please only submit bug reports when using the officially supported version.', - border, - ]; - extra.log(versionWarning.join('\n\n')); - } - warnedAboutTSVersion = true; - } -} - -/** - * ESLint (and therefore typescript-eslint) is used in both "single run"/one-time contexts, - * such as an ESLint CLI invocation, and long-running sessions (such as continuous feedback - * on a file in an IDE). - * - * When typescript-eslint handles TypeScript Program management behind the scenes, this distinction - * is important because there is significant overhead to managing the so called Watch Programs - * needed for the long-running use-case. We therefore use the following logic to figure out which - * of these contexts applies to the current execution. - */ -function inferSingleRun(options: TSESTreeOptions | undefined): void { - // Allow users to explicitly inform us of their intent to perform a single run (or not) with TSESTREE_SINGLE_RUN - if (process.env.TSESTREE_SINGLE_RUN === 'false') { - extra.singleRun = false; - return; - } - if (process.env.TSESTREE_SINGLE_RUN === 'true') { - extra.singleRun = true; - return; - } - - // Currently behind a flag while we gather real-world feedback - if (options?.allowAutomaticSingleRunInference) { - if ( - // Default to single runs for CI processes. CI=true is set by most CI providers by default. - process.env.CI === 'true' || - // This will be true for invocations such as `npx eslint ...` and `./node_modules/.bin/eslint ...` - process.argv[1].endsWith(normalize('node_modules/.bin/eslint')) - ) { - extra.singleRun = true; - return; - } - } - - /** - * We default to assuming that this run could be part of a long-running session (e.g. in an IDE) - * and watch programs will therefore be required - */ - extra.singleRun = false; -} - // eslint-disable-next-line @typescript-eslint/no-empty-interface interface EmptyObject {} type AST = TSESTree.Program & @@ -433,7 +80,7 @@ function parseWithNodeMapsInternal( /** * Reset the parse configuration */ - resetExtra(); + const parseSettings = createParseSettings(code, options); /** * Ensure users do not attempt to use parse() when they need parseAndGenerateServices() @@ -445,38 +92,18 @@ function parseWithNodeMapsInternal( } /** - * Ensure the source code is a string, and store a reference to it - */ - code = enforceString(code); - extra.code = code; - - /** - * Apply the given parser options - */ - if (typeof options !== 'undefined') { - applyParserOptionsToExtra(options); - } - - /** - * Warn if the user is using an unsupported version of TypeScript - */ - warnAboutTSVersion(); - - /** - * Figure out whether this is a single run or part of a long-running process + * Create a ts.SourceFile directly, no ts.Program is needed for a simple parse */ - inferSingleRun(options); - - /** - * Create a ts.SourceFile directly, no ts.Program is needed for a simple - * parse - */ - const ast = createSourceFile(code, extra); + const ast = createSourceFile(parseSettings.code, parseSettings); /** * Convert the TypeScript AST to an ESTree-compatible one */ - const { estree, astMaps } = astConverter(ast, extra, shouldPreserveNodeMaps); + const { estree, astMaps } = astConverter( + ast, + parseSettings, + shouldPreserveNodeMaps, + ); return { ast: estree as AST, @@ -505,47 +132,31 @@ function parseAndGenerateServices( /** * Reset the parse configuration */ - resetExtra(); + const parseSettings = createParseSettings(code, options); - /** - * Ensure the source code is a string, and store a reference to it - */ - code = enforceString(code); - extra.code = code; - - /** - * Apply the given parser options - */ if (typeof options !== 'undefined') { - applyParserOptionsToExtra(options); if ( typeof options.errorOnTypeScriptSyntacticAndSemanticIssues === 'boolean' && options.errorOnTypeScriptSyntacticAndSemanticIssues ) { - extra.errorOnTypeScriptSyntacticAndSemanticIssues = true; + parseSettings.errorOnTypeScriptSyntacticAndSemanticIssues = true; } } - /** - * Warn if the user is using an unsupported version of TypeScript - */ - warnAboutTSVersion(); - - /** - * Figure out whether this is a single run or part of a long-running process - */ - inferSingleRun(options); - /** * If this is a single run in which the user has not provided any existing programs but there * are programs which need to be created from the provided "project" option, * create an Iterable which will lazily create the programs as needed by the iteration logic */ - if (extra.singleRun && !extra.programs && extra.projects?.length > 0) { - extra.programs = { + if ( + parseSettings.singleRun && + !parseSettings.programs && + parseSettings.projects?.length > 0 + ) { + parseSettings.programs = { *[Symbol.iterator](): Iterator { - for (const configFile of extra.projects) { + for (const configFile of parseSettings.projects) { const existingProgram = existingPrograms.get(configFile); if (existingProgram) { yield existingProgram; @@ -567,7 +178,7 @@ function parseAndGenerateServices( * Generate a full ts.Program or offer provided instances in order to be able to provide parser services, such as type-checking */ const shouldProvideParserServices = - extra.programs != null || (extra.projects && extra.projects.length > 0); + parseSettings.programs != null || parseSettings.projects?.length > 0; /** * If we are in singleRun mode but the parseAndGenerateServices() function has been called more than once for the current file, @@ -577,46 +188,38 @@ 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. */ - let ast: ts.SourceFile; - let program: ts.Program; - - if (extra.singleRun && options.filePath) { + if (parseSettings.singleRun && options.filePath) { parseAndGenerateServicesCalls[options.filePath] = (parseAndGenerateServicesCalls[options.filePath] || 0) + 1; } - if ( - extra.singleRun && + const { ast, program } = + parseSettings.singleRun && options.filePath && parseAndGenerateServicesCalls[options.filePath] > 1 - ) { - const isolatedAstAndProgram = createIsolatedProgram(code, extra); - ast = isolatedAstAndProgram.ast; - program = isolatedAstAndProgram.program; - } else { - const astAndProgram = getProgramAndAST( - code, - extra.programs, - shouldProvideParserServices, - extra.createDefaultProgram, - )!; - ast = astAndProgram.ast; - program = astAndProgram.program; - } + ? createIsolatedProgram(parseSettings) + : getProgramAndAST(parseSettings, shouldProvideParserServices)!; /** * Convert the TypeScript AST to an ESTree-compatible one, and optionally preserve * mappings between converted and original AST nodes */ - const preserveNodeMaps = - typeof extra.preserveNodeMaps === 'boolean' ? extra.preserveNodeMaps : true; - const { estree, astMaps } = astConverter(ast, extra, preserveNodeMaps); + const shouldPreserveNodeMaps = + typeof parseSettings.preserveNodeMaps === 'boolean' + ? parseSettings.preserveNodeMaps + : true; + + const { estree, astMaps } = astConverter( + ast, + parseSettings, + shouldPreserveNodeMaps, + ); /** * Even if TypeScript parsed the source code ok, and we had no problems converting the AST, * there may be other syntactic or semantic issues in the code that we can optionally report on. */ - if (program && extra.errorOnTypeScriptSyntacticAndSemanticIssues) { + if (program && parseSettings.errorOnTypeScriptSyntacticAndSemanticIssues) { const error = getFirstSemanticOrSyntacticError(program, ast); if (error) { throw convertError(error); diff --git a/packages/typescript-estree/src/warnAboutTSVersion.ts b/packages/typescript-estree/src/warnAboutTSVersion.ts new file mode 100644 index 00000000000..03ca13354b5 --- /dev/null +++ b/packages/typescript-estree/src/warnAboutTSVersion.ts @@ -0,0 +1,45 @@ +import semver from 'semver'; +import * as ts from 'typescript'; + +import type { ParseSettings } from './parseSettings'; +/** + * This needs to be kept in sync with the top-level README.md in the + * typescript-eslint monorepo + */ +const SUPPORTED_TYPESCRIPT_VERSIONS = '>=3.3.1 <4.9.0'; + +/* + * The semver package will ignore prerelease ranges, and we don't want to explicitly document every one + * List them all separately here, so we can automatically create the full string + */ +const SUPPORTED_PRERELEASE_RANGES: string[] = []; +const ACTIVE_TYPESCRIPT_VERSION = ts.version; +const isRunningSupportedTypeScriptVersion = semver.satisfies( + ACTIVE_TYPESCRIPT_VERSION, + [SUPPORTED_TYPESCRIPT_VERSIONS] + .concat(SUPPORTED_PRERELEASE_RANGES) + .join(' || '), +); + +let warnedAboutTSVersion = false; + +export function warnAboutTSVersion(parseSettings: ParseSettings): void { + if (!isRunningSupportedTypeScriptVersion && !warnedAboutTSVersion) { + const isTTY = + typeof process === 'undefined' ? false : process.stdout?.isTTY; + if (isTTY) { + const border = '============='; + const versionWarning = [ + border, + 'WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.', + 'You may find that it works just fine, or you may not.', + `SUPPORTED TYPESCRIPT VERSIONS: ${SUPPORTED_TYPESCRIPT_VERSIONS}`, + `YOUR TYPESCRIPT VERSION: ${ACTIVE_TYPESCRIPT_VERSION}`, + 'Please only submit bug reports when using the officially supported version.', + border, + ]; + parseSettings.log(versionWarning.join('\n\n')); + } + warnedAboutTSVersion = true; + } +} diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts index e3fa23cf0e0..378e34ed136 100644 --- a/packages/website/src/components/linter/WebLinter.ts +++ b/packages/website/src/components/linter/WebLinter.ts @@ -1,5 +1,5 @@ import { createVirtualCompilerHost } from '@site/src/components/linter/CompilerHost'; -import { extra } from '@site/src/components/linter/config'; +import { parseSettings } from '@site/src/components/linter/config'; import type { ParserOptions } from '@typescript-eslint/types'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import type { LintUtils } from '@typescript-eslint/website-eslint'; @@ -106,7 +106,7 @@ export class WebLinter { const { estree: ast, astMaps } = this.lintUtils.astConverter( tsAst, - { ...extra, code, jsx: isJsx }, + { ...parseSettings, code, jsx: isJsx }, true, ); diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index cad38a3e28f..f077f3786ee 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -1,12 +1,11 @@ -import type { Extra } from '@typescript-eslint/typescript-estree/dist/parser-options'; +import type { ParseSettings } from '@typescript-eslint/typescript-estree/dist/parseSettings'; -export const extra: Extra = { +export const parseSettings: ParseSettings = { code: '', comment: true, comments: [], createDefaultProgram: false, debugLevel: new Set(), - errorOnTypeScriptSyntacticAndSemanticIssues: false, errorOnUnknownASTType: false, extraFileExtensions: [], filePath: '', @@ -17,9 +16,9 @@ export const extra: Extra = { preserveNodeMaps: true, projects: [], range: true, - strict: false, tokens: [], tsconfigRootDir: '/', + errorOnTypeScriptSyntacticAndSemanticIssues: false, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: false, singleRun: false, programs: null, From 25f15096923cbc2b78698d67195ee20baf14f6f1 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 24 Oct 2022 15:09:05 -0400 Subject: [PATCH 2/4] Broke up type cycle and made it readonly --- .../src/create-program/createSourceFile.ts | 7 +- packages/typescript-estree/src/log.ts | 3 - .../createParseSettings.ts} | 136 ++---------------- .../src/parseSettings/index.ts | 124 ++++++++++++++++ .../{ => parseSettings}/warnAboutTSVersion.ts | 2 +- packages/typescript-estree/src/parser.ts | 8 +- 6 files changed, 142 insertions(+), 138 deletions(-) delete mode 100644 packages/typescript-estree/src/log.ts rename packages/typescript-estree/src/{parseSettings.ts => parseSettings/createParseSettings.ts} (65%) create mode 100644 packages/typescript-estree/src/parseSettings/index.ts rename packages/typescript-estree/src/{ => parseSettings}/warnAboutTSVersion.ts (96%) diff --git a/packages/typescript-estree/src/create-program/createSourceFile.ts b/packages/typescript-estree/src/create-program/createSourceFile.ts index 5d98266ad55..806e503f0e4 100644 --- a/packages/typescript-estree/src/create-program/createSourceFile.ts +++ b/packages/typescript-estree/src/create-program/createSourceFile.ts @@ -6,10 +6,7 @@ import { getScriptKind } from './getScriptKind'; const log = debug('typescript-eslint:typescript-estree:createSourceFile'); -function createSourceFile( - code: string, - parseSettings: ParseSettings, -): ts.SourceFile { +function createSourceFile(parseSettings: ParseSettings): ts.SourceFile { log( 'Getting AST without type information in %s mode for: %s', parseSettings.jsx ? 'TSX' : 'TS', @@ -18,7 +15,7 @@ function createSourceFile( return ts.createSourceFile( parseSettings.filePath, - code, + parseSettings.code, ts.ScriptTarget.Latest, /* setParentNodes */ true, getScriptKind(parseSettings.filePath, parseSettings.jsx), diff --git a/packages/typescript-estree/src/log.ts b/packages/typescript-estree/src/log.ts deleted file mode 100644 index 6bb628bf1ce..00000000000 --- a/packages/typescript-estree/src/log.ts +++ /dev/null @@ -1,3 +0,0 @@ -import debug from 'debug'; - -export const log = debug('typescript-eslint:typescript-estree:parser'); diff --git a/packages/typescript-estree/src/parseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts similarity index 65% rename from packages/typescript-estree/src/parseSettings.ts rename to packages/typescript-estree/src/parseSettings/createParseSettings.ts index f24ae7cf5b1..a2d87bfed53 100644 --- a/packages/typescript-estree/src/parseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -1,146 +1,30 @@ import debug from 'debug'; import { sync as globSync } from 'globby'; import isGlob from 'is-glob'; -import type * as ts from 'typescript'; -import type { CanonicalPath } from './create-program/shared'; +import type { CanonicalPath } from '../create-program/shared'; import { ensureAbsolutePath, getCanonicalFileName, -} from './create-program/shared'; -import { inferSingleRun } from './inferSingleRun'; -import { log } from './log'; -import type { TSESTreeOptions } from './parser-options'; -import type { TSESTree } from './ts-estree'; +} from '../create-program/shared'; +import { inferSingleRun } from '../inferSingleRun'; +import type { TSESTreeOptions } from '../parser-options'; +import type { MutableParseSettings } from './index'; import { warnAboutTSVersion } from './warnAboutTSVersion'; -type DebugModule = 'typescript-eslint' | 'eslint' | 'typescript'; - -/** - * Internal settings used by the parser to run on a file. - */ -export interface ParseSettings { - /** - * Code of the file being parsed. - */ - code: string; - - /** - * Whether the `comment` parse option is enabled. - */ - comment: boolean; - - /** - * If the `comment` parse option is enabled, retrieved comments. - */ - comments: TSESTree.Comment[]; - - /** - * Whether to create a TypeScript program if one is not provided. - */ - createDefaultProgram: boolean; - - /** - * Which debug areas should be logged. - */ - debugLevel: Set; - - /** - * Whether to error if TypeScript reports a semantic or syntactic error diagnostic. - */ - errorOnTypeScriptSyntacticAndSemanticIssues: boolean; - - /** - * Whether to error if an unknown AST node type is encountered. - */ - errorOnUnknownASTType: boolean; - - /** - * Whether TS should use the source files for referenced projects instead of the compiled .d.ts files. - * - * @remarks - * This feature is not yet optimized, and is likely to cause OOMs for medium to large projects. - * This flag REQUIRES at least TS v3.9, otherwise it does nothing. - */ - EXPERIMENTAL_useSourceOfProjectReferenceRedirect: boolean; - - /** - * Any non-standard file extensions which will be parsed. - */ - extraFileExtensions: string[]; - - /** - * Path of the file being parsed. - */ - filePath: string; - - /** - * Whether parsing of JSX is enabled. - * - * @remarks The applicable file extension is still required. - */ - jsx: boolean; - - /** - * Whether to add `loc` information to each node. - */ - loc: boolean; - - /** - * Log function, if not `console.log`. - */ - log: (message: string) => void; - - /** - * Path for a module resolver to use for the compiler host's `resolveModuleNames`. - */ - moduleResolver: string; - - /** - * Whether two-way AST node maps are preserved during the AST conversion process. - */ - preserveNodeMaps?: boolean; - - /** - * One or more instances of TypeScript Program objects to be used for type information. - */ - programs: null | Iterable; - - /** - * Normalized paths to provided project paths. - */ - projects: CanonicalPath[]; - - /** - * Whether to add the `range` property to AST nodes. - */ - range: boolean; - - /** - * Whether this is part of a single run, rather than a long-running process. - */ - singleRun: boolean; - - /** - * If the `cotokensmment` parse option is enabled, retrieved tokens. - */ - tokens: null | TSESTree.Token[]; - - /** - * The absolute path to the root directory for all provided `project`s. - */ - tsconfigRootDir: string; -} +const log = debug( + 'typescript-eslint:typescript-estree:parser:parseSettings/createParseSettings', +); export function createParseSettings( code: string, options: Partial = {}, -): ParseSettings { +): MutableParseSettings { const tsconfigRootDir = typeof options.tsconfigRootDir === 'string' ? options.tsconfigRootDir : process.cwd(); - const parseSettings: ParseSettings = { + const parseSettings: MutableParseSettings = { code: enforceString(code), comment: options.comment === true, comments: [], diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts new file mode 100644 index 00000000000..4d8f377f675 --- /dev/null +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -0,0 +1,124 @@ +import type * as ts from 'typescript'; + +import type { CanonicalPath } from '../create-program/shared'; +import type { TSESTree } from '../ts-estree'; + +type DebugModule = 'typescript-eslint' | 'eslint' | 'typescript'; + +/** + * Internal settings used by the parser to run on a file. + */ +export interface MutableParseSettings { + /** + * Code of the file being parsed. + */ + code: string; + + /** + * Whether the `comment` parse option is enabled. + */ + comment: boolean; + + /** + * If the `comment` parse option is enabled, retrieved comments. + */ + comments: TSESTree.Comment[]; + + /** + * Whether to create a TypeScript program if one is not provided. + */ + createDefaultProgram: boolean; + + /** + * Which debug areas should be logged. + */ + debugLevel: Set; + + /** + * Whether to error if TypeScript reports a semantic or syntactic error diagnostic. + */ + errorOnTypeScriptSyntacticAndSemanticIssues: boolean; + + /** + * Whether to error if an unknown AST node type is encountered. + */ + errorOnUnknownASTType: boolean; + + /** + * Whether TS should use the source files for referenced projects instead of the compiled .d.ts files. + * + * @remarks + * This feature is not yet optimized, and is likely to cause OOMs for medium to large projects. + * This flag REQUIRES at least TS v3.9, otherwise it does nothing. + */ + EXPERIMENTAL_useSourceOfProjectReferenceRedirect: boolean; + + /** + * Any non-standard file extensions which will be parsed. + */ + extraFileExtensions: string[]; + + /** + * Path of the file being parsed. + */ + filePath: string; + + /** + * Whether parsing of JSX is enabled. + * + * @remarks The applicable file extension is still required. + */ + jsx: boolean; + + /** + * Whether to add `loc` information to each node. + */ + loc: boolean; + + /** + * Log function, if not `console.log`. + */ + log: (message: string) => void; + + /** + * Path for a module resolver to use for the compiler host's `resolveModuleNames`. + */ + moduleResolver: string; + + /** + * Whether two-way AST node maps are preserved during the AST conversion process. + */ + preserveNodeMaps?: boolean; + + /** + * One or more instances of TypeScript Program objects to be used for type information. + */ + programs: null | Iterable; + + /** + * Normalized paths to provided project paths. + */ + projects: CanonicalPath[]; + + /** + * Whether to add the `range` property to AST nodes. + */ + range: boolean; + + /** + * Whether this is part of a single run, rather than a long-running process. + */ + singleRun: boolean; + + /** + * If the `cotokensmment` parse option is enabled, retrieved tokens. + */ + tokens: null | TSESTree.Token[]; + + /** + * The absolute path to the root directory for all provided `project`s. + */ + tsconfigRootDir: string; +} + +export type ParseSettings = Readonly; diff --git a/packages/typescript-estree/src/warnAboutTSVersion.ts b/packages/typescript-estree/src/parseSettings/warnAboutTSVersion.ts similarity index 96% rename from packages/typescript-estree/src/warnAboutTSVersion.ts rename to packages/typescript-estree/src/parseSettings/warnAboutTSVersion.ts index 03ca13354b5..6bd890bdae9 100644 --- a/packages/typescript-estree/src/warnAboutTSVersion.ts +++ b/packages/typescript-estree/src/parseSettings/warnAboutTSVersion.ts @@ -1,7 +1,7 @@ import semver from 'semver'; import * as ts from 'typescript'; -import type { ParseSettings } from './parseSettings'; +import type { ParseSettings } from './index'; /** * This needs to be kept in sync with the top-level README.md in the * typescript-eslint monorepo diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index a00caeee89c..c5504ba961a 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -1,3 +1,4 @@ +import debug from 'debug'; import type * as ts from 'typescript'; import { astConverter } from './ast-converter'; @@ -11,13 +12,14 @@ import { createProgramFromConfigFile, useProvidedPrograms, } from './create-program/useProvidedPrograms'; -import { log } from './log'; import type { ParserServices, TSESTreeOptions } from './parser-options'; import type { ParseSettings } from './parseSettings'; -import { createParseSettings } from './parseSettings'; +import { createParseSettings } from './parseSettings/createParseSettings'; import { getFirstSemanticOrSyntacticError } from './semantic-or-syntactic-errors'; import type { TSESTree } from './ts-estree'; +const log = debug('typescript-eslint:typescript-estree:parser'); + /** * Cache existing programs for the single run use-case. * @@ -94,7 +96,7 @@ function parseWithNodeMapsInternal( /** * Create a ts.SourceFile directly, no ts.Program is needed for a simple parse */ - const ast = createSourceFile(parseSettings.code, parseSettings); + const ast = createSourceFile(parseSettings); /** * Convert the TypeScript AST to an ESTree-compatible one From 7b4f8d868d8544d636e66fcc754bb8aa11632999 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 25 Oct 2022 09:49:07 -0400 Subject: [PATCH 3/4] Update packages/typescript-estree/src/parseSettings/createParseSettings.ts Co-authored-by: Brad Zacher --- .../typescript-estree/src/parseSettings/createParseSettings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index a2d87bfed53..36933fd637e 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -13,7 +13,7 @@ import type { MutableParseSettings } from './index'; import { warnAboutTSVersion } from './warnAboutTSVersion'; const log = debug( - 'typescript-eslint:typescript-estree:parser:parseSettings/createParseSettings', + 'typescript-eslint:typescript-estree:parser:parseSettings:createParseSettings', ); export function createParseSettings( From 7b7d2e8d4d98bb23b90c438ab2ceac4caf12161d Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 25 Oct 2022 09:58:45 -0400 Subject: [PATCH 4/4] Update packages/typescript-estree/src/parseSettings/index.ts --- packages/typescript-estree/src/parseSettings/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index 4d8f377f675..0a9734d1b24 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -111,7 +111,7 @@ export interface MutableParseSettings { singleRun: boolean; /** - * If the `cotokensmment` parse option is enabled, retrieved tokens. + * If the `tokens` parse option is enabled, retrieved tokens. */ tokens: null | TSESTree.Token[];