diff --git a/packages/typescript-estree/src/create-program/createProjectProgram.ts b/packages/typescript-estree/src/create-program/createProjectProgram.ts index 784b44b93d4d..5a5cb11fd425 100644 --- a/packages/typescript-estree/src/create-program/createProjectProgram.ts +++ b/packages/typescript-estree/src/create-program/createProjectProgram.ts @@ -4,8 +4,7 @@ import * as ts from 'typescript'; import { firstDefined } from '../node-utils'; import type { ParseSettings } from '../parseSettings'; -import { getWatchProgramsForProjects } from './getWatchProgramsForProjects'; -import type { ASTAndProgram } from './shared'; +import type { ASTAndProgram, CanonicalPath } from './shared'; import { getAstFromProgram } from './shared'; const log = debug('typescript-eslint:typescript-estree:createProjectProgram'); @@ -27,10 +26,10 @@ const DEFAULT_EXTRA_FILE_EXTENSIONS = [ */ function createProjectProgram( parseSettings: ParseSettings, + programsForProjects: readonly ts.Program[], ): ASTAndProgram | undefined { log('Creating project program for: %s', parseSettings.filePath); - const programsForProjects = getWatchProgramsForProjects(parseSettings); const astAndProgram = firstDefined(programsForProjects, currentProgram => getAstFromProgram(currentProgram, parseSettings), ); @@ -40,7 +39,7 @@ function createProjectProgram( return astAndProgram; } - const describeFilePath = (filePath: string): string => { + const describeFilePath = (filePath: CanonicalPath): string => { const relative = path.relative( parseSettings.tsconfigRootDir || process.cwd(), filePath, diff --git a/packages/typescript-estree/src/create-program/getLanguageServiceProgram.ts b/packages/typescript-estree/src/create-program/getLanguageServiceProgram.ts new file mode 100644 index 000000000000..a75b22867ffc --- /dev/null +++ b/packages/typescript-estree/src/create-program/getLanguageServiceProgram.ts @@ -0,0 +1,336 @@ +import debug from 'debug'; +import * as ts from 'typescript'; + +import type { ParseSettings } from '../parseSettings'; +import { getScriptKind } from './getScriptKind'; +import type { CanonicalPath, FileHash, TSConfigCanonicalPath } from './shared'; +import { + createDefaultCompilerOptionsFromExtra, + createHash, + getCanonicalFileName, + registerAdditionalCacheClearer, + useCaseSensitiveFileNames, +} from './shared'; + +const log = debug( + 'typescript-eslint:typescript-estree:getLanguageServiceProgram', +); + +type KnownLanguageService = Readonly<{ + configFile: ts.ParsedCommandLine; + fileList: ReadonlySet; + languageService: ts.LanguageService; +}>; +/** + * Maps tsconfig paths to their corresponding file contents and resulting watches + */ +const knownLanguageServiceMap = new Map< + TSConfigCanonicalPath, + KnownLanguageService +>(); + +type CachedFile = Readonly<{ + hash: FileHash; + snapshot: ts.IScriptSnapshot; + // starts at 0 and increments each time we see new text for the file + version: number; +}>; +/** + * Stores the hashes of files so we know if we need to inform TS of file changes. + */ +const parsedFileCache = new Map(); + +registerAdditionalCacheClearer(() => { + knownLanguageServiceMap.clear(); + parsedFileCache.clear(); + documentRegistry = null; +}); + +/** + * Holds information about the file currently being linted + */ +const currentLintOperationState: { code: string; filePath: CanonicalPath } = { + code: '', + filePath: '' as CanonicalPath, +}; + +/** + * Persistent text document registry that shares text documents across programs to + * reduce memory overhead. + * + * We don't initialize this until the first time we run the code. + */ +let documentRegistry: ts.DocumentRegistry | null; + +function maybeUpdateFile( + filePath: CanonicalPath, + fileContents: string | undefined, + parseSettings: ParseSettings, +): boolean { + if (fileContents == null || documentRegistry == null) { + return false; + } + + const newCodeHash = createHash(fileContents); + const cachedParsedFile = parsedFileCache.get(filePath); + if (cachedParsedFile?.hash === newCodeHash) { + // nothing needs updating + return false; + } + + const snapshot = ts.ScriptSnapshot.fromString(fileContents); + const version = (cachedParsedFile?.version ?? 0) + 1; + parsedFileCache.set(filePath, { + hash: newCodeHash, + snapshot, + version, + }); + + for (const { configFile } of knownLanguageServiceMap.values()) { + /* + TODO - this isn't safe or correct. + + When the user edits a file IDE integrations will run ESLint on the unsaved text. + This will cause us to update our registry with the new "dirty" text content. + + If the user saves the file, then dirty becomes clean and we're happy because + when the user edits the next file we've already updated our state. + + However if the user closes the file without saving, then the registry will be + stuck with the dirty text, which could cause issues that can only be fixed by + either (a) restarting the IDE or (b) opening the clean file again. + + This is the reason that the builder program version doesn't re-use the + current parsed text any longer than the duration of the current parse. + + Problem notes: + - we can't attach disk watchers because we don't know if we're in a CLI or an + IDE environment. This means we don't know when a change is committed for a + file. + - ESLint has there's no mechanism to tell us when the lint run is done, so + we don't know when it's safe to roll-back the update. + - maybe this doesn't matter and we can just roll-back the change after + we finish the current parse (i.e. return the dirty program?). + - we don't own the IDE integration so we don't know when a file closes in a + dirty state, nor do we know when a file is opened in a clean state. + + TODO for now. Will need to solve before we can consider releasing. + */ + documentRegistry.updateDocument( + filePath, + configFile.options, + snapshot, + version.toString(), + getScriptKind(filePath, parseSettings.jsx), + ); + } + + return true; +} + +export function getLanguageServiceProgram( + parseSettings: ParseSettings, +): ts.Program[] { + if (!documentRegistry) { + documentRegistry = ts.createDocumentRegistry( + useCaseSensitiveFileNames, + process.cwd(), + ); + } + + const filePath = getCanonicalFileName(parseSettings.filePath); + + // preserve reference to code and file being linted + currentLintOperationState.code = parseSettings.code; + currentLintOperationState.filePath = filePath; + + // Update file version if necessary + maybeUpdateFile(filePath, parseSettings.code, parseSettings); + + const currentProjectsFromSettings = new Set(parseSettings.projects); + + /* + * before we go into the process of attempting to find and update every program + * see if we know of a program that contains this file + */ + for (const [ + tsconfigPath, + { fileList, languageService }, + ] of knownLanguageServiceMap.entries()) { + if (!currentProjectsFromSettings.has(tsconfigPath)) { + // the current parser run doesn't specify this tsconfig in parserOptions.project + // so we don't want to consider it for caching purposes. + // + // if we did consider it we might return a program for a project + // that wasn't specified in the current parser run (which is obv bad!). + continue; + } + + if (fileList.has(filePath)) { + log('Found existing language service - %s', tsconfigPath); + + const updatedProgram = languageService.getProgram(); + if (!updatedProgram) { + log( + 'Could not get program from language service for project %s', + tsconfigPath, + ); + continue; + } + // TODO - do we need this? + // sets parent pointers in source files + // updatedProgram.getTypeChecker(); + + return [updatedProgram]; + } + } + log( + 'File did not belong to any existing language services, moving to create/update. %s', + filePath, + ); + + const results = []; + + /* + * We don't know of a program that contains the file, this means that either: + * - 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 parseSettings.projects) { + const existingLanguageService = knownLanguageServiceMap.get(tsconfigPath); + + if (existingLanguageService) { + const result = createLanguageService(tsconfigPath, parseSettings); + if (result == null) { + log('could not update language service %s', tsconfigPath); + continue; + } + const updatedProgram = result.program; + + // TODO - do we need this? + // sets parent pointers in source files + // updatedProgram.getTypeChecker(); + + // cache and check the file list + const fileList = existingLanguageService.fileList; + if (fileList.has(filePath)) { + log('Found updated program %s', tsconfigPath); + // we can return early because we know this program contains the file + return [updatedProgram]; + } + + results.push(updatedProgram); + continue; + } + + const result = createLanguageService(tsconfigPath, parseSettings); + if (result == null) { + continue; + } + + const { fileList, program } = result; + + // cache and check the file list + if (fileList.has(filePath)) { + log('Found program for file. %s', filePath); + // we can return early because we know this program contains the file + return [program]; + } + + results.push(program); + } + + return results; +} + +function createLanguageService( + tsconfigPath: TSConfigCanonicalPath, + parseSettings: ParseSettings, +): { fileList: ReadonlySet; program: ts.Program } | null { + const configFile = ts.getParsedCommandLineOfConfigFile( + tsconfigPath, + createDefaultCompilerOptionsFromExtra(parseSettings), + { + ...ts.sys, + onUnRecoverableConfigFileDiagnostic: diagnostic => { + throw new Error( + ts.flattenDiagnosticMessageText( + diagnostic.messageText, + ts.sys.newLine, + ), + ); + }, + }, + ); + if (configFile == null) { + // this should be unreachable because we throw on unrecoverable diagnostics + log('Unable to parse config file %s', tsconfigPath); + return null; + } + + const host: ts.LanguageServiceHost = { + ...ts.sys, + getCompilationSettings: () => configFile.options, + getScriptFileNames: () => configFile.fileNames, + getScriptVersion: filePathIn => { + const filePath = getCanonicalFileName(filePathIn); + return parsedFileCache.get(filePath)?.version.toString(10) ?? '0'; + }, + getScriptSnapshot: filePathIn => { + const filePath = getCanonicalFileName(filePathIn); + const cached = parsedFileCache.get(filePath); + if (cached) { + return cached.snapshot; + } + + const contents = host.readFile(filePathIn); + if (contents == null) { + return undefined; + } + + return ts.ScriptSnapshot.fromString(contents); + }, + getDefaultLibFileName: ts.getDefaultLibFileName, + readFile: (filePathIn, encoding) => { + const filePath = getCanonicalFileName(filePathIn); + const cached = parsedFileCache.get(filePath); + if (cached) { + return cached.snapshot.getText(0, cached.snapshot.getLength()); + } + + const fileContent = + filePath === currentLintOperationState.filePath + ? currentLintOperationState.code + : ts.sys.readFile(filePath, encoding); + maybeUpdateFile(filePath, fileContent, parseSettings); + return fileContent; + }, + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, + }; + + if (documentRegistry == null) { + // should be impossible to reach + throw new Error( + 'Unexpected state - document registry was not initialized.', + ); + } + + const languageService = ts.createLanguageService(host, documentRegistry); + const fileList = new Set(configFile.fileNames.map(getCanonicalFileName)); + knownLanguageServiceMap.set(tsconfigPath, { + configFile, + fileList, + languageService, + }); + + const program = languageService.getProgram(); + if (program == null) { + log( + 'Unable to get program from language service for config %s', + tsconfigPath, + ); + return null; + } + + return { fileList, program }; +} diff --git a/packages/typescript-estree/src/create-program/getWatchProgramsForProjects.ts b/packages/typescript-estree/src/create-program/getWatchProgramsForProjects.ts index 15d88e5f4540..ad5d85292114 100644 --- a/packages/typescript-estree/src/create-program/getWatchProgramsForProjects.ts +++ b/packages/typescript-estree/src/create-program/getWatchProgramsForProjects.ts @@ -4,22 +4,27 @@ import semver from 'semver'; import * as ts from 'typescript'; import type { ParseSettings } from '../parseSettings'; -import type { CanonicalPath } from './shared'; +import type { CanonicalPath, FileHash, TSConfigCanonicalPath } from './shared'; import { canonicalDirname, createDefaultCompilerOptionsFromExtra, + createHash, getCanonicalFileName, getModuleResolver, + hasTSConfigChanged, + registerAdditionalCacheClearer, } from './shared'; import type { WatchCompilerHostOfConfigFile } from './WatchCompilerHostOfConfigFile'; -const log = debug('typescript-eslint:typescript-estree:createWatchProgram'); +const log = debug( + 'typescript-eslint:typescript-estree:getWatchProgramsForProjects', +); /** * Maps tsconfig paths to their corresponding file contents and resulting watches */ const knownWatchProgramMap = new Map< - CanonicalPath, + TSConfigCanonicalPath, ts.WatchOfConfigFile >(); @@ -39,27 +44,23 @@ const folderWatchCallbackTrackingMap = new Map< /** * Stores the list of known files for each program */ -const programFileListCache = new Map>(); +const programFileListCache = new Map< + TSConfigCanonicalPath, + Set +>(); /** - * Caches the last modified time of the tsconfig files + * Stores the hashes of files so we know if we need to inform TS of file changes. */ -const tsconfigLastModifiedTimestampCache = new Map(); - -const parsedFilesSeenHash = new Map(); +const parsedFilesSeenHash = new Map(); -/** - * Clear all of the parser caches. - * This should only be used in testing to ensure the parser is clean between tests. - */ -function clearWatchCaches(): void { +registerAdditionalCacheClearer(() => { knownWatchProgramMap.clear(); fileWatchCallbackTrackingMap.clear(); folderWatchCallbackTrackingMap.clear(); parsedFilesSeenHash.clear(); programFileListCache.clear(); - tsconfigLastModifiedTimestampCache.clear(); -} +}); function saveWatchCallback( trackingMap: Map>, @@ -105,21 +106,8 @@ function diagnosticReporter(diagnostic: ts.Diagnostic): void { ); } -/** - * Hash content for compare content. - * @param content hashed contend - * @returns hashed result - */ -function createHash(content: string): string { - // No ts.sys in browser environments. - if (ts.sys?.createHash) { - return ts.sys.createHash(content); - } - return content; -} - function updateCachedFileList( - tsconfigPath: CanonicalPath, + tsconfigPath: TSConfigCanonicalPath, program: ts.Program, parseSettings: ParseSettings, ): Set { @@ -142,7 +130,6 @@ function getWatchProgramsForProjects( parseSettings: ParseSettings, ): ts.Program[] { const filePath = getCanonicalFileName(parseSettings.filePath); - const results = []; // preserve reference to code and file being linted currentLintOperationState.code = parseSettings.code; @@ -203,6 +190,8 @@ function getWatchProgramsForProjects( filePath, ); + const results = []; + /* * We don't know of a program that contains the file, this means that either: * - the required program hasn't been created yet, or @@ -405,25 +394,10 @@ function createWatchProgram( return watch; } -function hasTSConfigChanged(tsconfigPath: CanonicalPath): boolean { - const stat = fs.statSync(tsconfigPath); - const lastModifiedAt = stat.mtimeMs; - const cachedLastModifiedAt = - tsconfigLastModifiedTimestampCache.get(tsconfigPath); - - tsconfigLastModifiedTimestampCache.set(tsconfigPath, lastModifiedAt); - - if (cachedLastModifiedAt === undefined) { - return false; - } - - return Math.abs(cachedLastModifiedAt - lastModifiedAt) > Number.EPSILON; -} - function maybeInvalidateProgram( existingWatch: ts.WatchOfConfigFile, filePath: CanonicalPath, - tsconfigPath: CanonicalPath, + tsconfigPath: TSConfigCanonicalPath, ): ts.Program | null { /* * By calling watchProgram.getProgram(), it will trigger a resync of the program based on @@ -550,4 +524,4 @@ function maybeInvalidateProgram( return null; } -export { clearWatchCaches, getWatchProgramsForProjects }; +export { getWatchProgramsForProjects }; diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index dd50f757dce1..9285329834cf 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import path from 'path'; import type { Program } from 'typescript'; import * as ts from 'typescript'; @@ -33,7 +34,7 @@ const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { checkJs: true, }; -function createDefaultCompilerOptionsFromExtra( +function createDefaultCompilerOptionsFromParseSettings( parseSettings: ParseSettings, ): ts.CompilerOptions { if (parseSettings.debugLevel.has('typescript')) { @@ -47,7 +48,10 @@ function createDefaultCompilerOptionsFromExtra( } // This narrows the type so we can be sure we're passing canonical names in the correct places -type CanonicalPath = string & { __brand: unknown }; +type CanonicalPath = string & { __canonicalPathBrand: unknown }; +type TSConfigCanonicalPath = CanonicalPath & { + __tsconfigCanonicalPathBrand: unknown; +}; // typescript doesn't provide a ts.sys implementation for browser environments const useCaseSensitiveFileNames = @@ -64,10 +68,14 @@ function getCanonicalFileName(filePath: string): CanonicalPath { return correctPathCasing(normalized) as CanonicalPath; } -function ensureAbsolutePath(p: string, tsconfigRootDir: string): string { - return path.isAbsolute(p) - ? p - : path.join(tsconfigRootDir || process.cwd(), p); +function getTsconfigCanonicalFileName(filePath: string): TSConfigCanonicalPath { + return getCanonicalFileName(filePath) as TSConfigCanonicalPath; +} + +function ensureAbsolutePath(p: string, tsconfigRootDir: string): CanonicalPath { + return getCanonicalFileName( + path.isAbsolute(p) ? p : path.join(tsconfigRootDir || process.cwd(), p), + ); } function canonicalDirname(p: CanonicalPath): CanonicalPath { @@ -124,14 +132,88 @@ function getModuleResolver(moduleResolverPath: string): ModuleResolver { return moduleResolver; } +/** + * Same fallback hashing algorithm TS uses: + * https://github.com/microsoft/TypeScript/blob/00dc0b6674eef3fbb3abb86f9d71705b11134446/src/compiler/sys.ts#L54-L66 + */ +function generateDjb2Hash(data: string): string { + let acc = 5381; + for (let i = 0; i < data.length; i++) { + acc = (acc << 5) + acc + data.charCodeAt(i); + } + return acc.toString(); +} + +type FileHash = string & { __fileHashBrand: unknown }; +/** + * Hash content for compare content. + * @param content hashed contend + * @returns hashed result + */ +function createHash(content: string): FileHash { + // No ts.sys in browser environments. + if (ts.sys?.createHash) { + return ts.sys.createHash(content) as FileHash; + } + return generateDjb2Hash(content) as FileHash; +} + +/** + * Caches the last modified time of the tsconfig files + */ +const tsconfigLastModifiedTimestampCache = new Map< + TSConfigCanonicalPath, + number +>(); + +function hasTSConfigChanged(tsconfigPath: TSConfigCanonicalPath): boolean { + const stat = fs.statSync(tsconfigPath); + const lastModifiedAt = stat.mtimeMs; + const cachedLastModifiedAt = + tsconfigLastModifiedTimestampCache.get(tsconfigPath); + + tsconfigLastModifiedTimestampCache.set(tsconfigPath, lastModifiedAt); + + if (cachedLastModifiedAt === undefined) { + return false; + } + + return Math.abs(cachedLastModifiedAt - lastModifiedAt) > Number.EPSILON; +} + +type CacheClearer = () => void; +const additionalCacheClearers: CacheClearer[] = []; + +/** + * Clear all of the parser caches. + * This should only be used in testing to ensure the parser is clean between tests. + */ +function clearWatchCaches(): void { + tsconfigLastModifiedTimestampCache.clear(); + for (const fn of additionalCacheClearers) { + fn(); + } +} +function registerAdditionalCacheClearer(fn: CacheClearer): void { + additionalCacheClearers.push(fn); +} + export { ASTAndProgram, CORE_COMPILER_OPTIONS, canonicalDirname, CanonicalPath, - createDefaultCompilerOptionsFromExtra, + clearWatchCaches, + createDefaultCompilerOptionsFromParseSettings as createDefaultCompilerOptionsFromExtra, + createHash, ensureAbsolutePath, + FileHash, getCanonicalFileName, getAstFromProgram, getModuleResolver, + getTsconfigCanonicalFileName, + hasTSConfigChanged, + registerAdditionalCacheClearer, + TSConfigCanonicalPath, + useCaseSensitiveFileNames, }; diff --git a/packages/typescript-estree/src/index.ts b/packages/typescript-estree/src/index.ts index bc7ed6024f3b..efb10b382228 100644 --- a/packages/typescript-estree/src/index.ts +++ b/packages/typescript-estree/src/index.ts @@ -10,7 +10,7 @@ export { export { ParserServices, TSESTreeOptions } from './parser-options'; export { simpleTraverse } from './simple-traverse'; export * from './ts-estree'; -export { clearWatchCaches as clearCaches } from './create-program/getWatchProgramsForProjects'; +export { clearWatchCaches as clearCaches } from './create-program/shared'; export { createProgramFromConfigFile as createProgram } from './create-program/useProvidedPrograms'; export * from './create-program/getScriptKind'; export { typescriptVersionIsAtLeast } from './version-check'; diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index b1cde9d4c9ad..f542ba0a5a5a 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -2,10 +2,11 @@ import debug from 'debug'; import { sync as globSync } from 'globby'; import isGlob from 'is-glob'; -import type { CanonicalPath } from '../create-program/shared'; +import type { TSConfigCanonicalPath } from '../create-program/shared'; import { ensureAbsolutePath, getCanonicalFileName, + getTsconfigCanonicalFileName, } from '../create-program/shared'; import type { TSESTreeOptions } from '../parser-options'; import type { MutableParseSettings } from './index'; @@ -39,6 +40,8 @@ export function createParseSettings( errorOnUnknownASTType: options.errorOnUnknownASTType === true, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === true, + EXPERIMENTAL_useLanguageService: + options.EXPERIMENTAL_useLanguageService === true, extraFileExtensions: Array.isArray(options.extraFileExtensions) && options.extraFileExtensions.every(ext => typeof ext === 'string') @@ -65,7 +68,7 @@ export function createParseSettings( range: options.range === true, singleRun: inferSingleRun(options), tokens: options.tokens === true ? [] : null, - tsconfigRootDir, + tsconfigRootDir: getCanonicalFileName(tsconfigRootDir), }; // debug doesn't support multiple `enable` calls, so have to do it all at once @@ -148,8 +151,8 @@ function getFileName(jsx?: boolean): string { function getTsconfigPath( tsconfigPath: string, tsconfigRootDir: string, -): CanonicalPath { - return getCanonicalFileName( +): TSConfigCanonicalPath { + return getTsconfigCanonicalFileName( ensureAbsolutePath(tsconfigPath, tsconfigRootDir), ); } @@ -161,7 +164,7 @@ function prepareAndTransformProjects( tsconfigRootDir: string, projectsInput: string | string[] | undefined, ignoreListInput: string[], -): CanonicalPath[] { +): TSConfigCanonicalPath[] { const sanitizedProjects: string[] = []; // Normalize and sanitize the project paths diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index 0a9734d1b241..636b9de25292 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -1,6 +1,9 @@ import type * as ts from 'typescript'; -import type { CanonicalPath } from '../create-program/shared'; +import type { + CanonicalPath, + TSConfigCanonicalPath, +} from '../create-program/shared'; import type { TSESTree } from '../ts-estree'; type DebugModule = 'typescript-eslint' | 'eslint' | 'typescript'; @@ -53,6 +56,11 @@ export interface MutableParseSettings { */ EXPERIMENTAL_useSourceOfProjectReferenceRedirect: boolean; + /** + * Whether to use a LanguageService for program management or not. + */ + EXPERIMENTAL_useLanguageService: boolean; + /** * Any non-standard file extensions which will be parsed. */ @@ -61,7 +69,7 @@ export interface MutableParseSettings { /** * Path of the file being parsed. */ - filePath: string; + filePath: CanonicalPath; /** * Whether parsing of JSX is enabled. @@ -98,7 +106,7 @@ export interface MutableParseSettings { /** * Normalized paths to provided project paths. */ - projects: CanonicalPath[]; + projects: TSConfigCanonicalPath[]; /** * Whether to add the `range` property to AST nodes. @@ -118,7 +126,7 @@ export interface MutableParseSettings { /** * The absolute path to the root directory for all provided `project`s. */ - tsconfigRootDir: string; + tsconfigRootDir: CanonicalPath; } export type ParseSettings = Readonly; diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 632d9e6ae883..b5507728c090 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -92,6 +92,13 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { */ EXPERIMENTAL_useSourceOfProjectReferenceRedirect?: boolean; + /** + * ***EXPERIMENTAL FLAG*** - Use this at your own risk. + * + * Manage type-aware parsing using a `ts.LanguageService` instead of a `ts.BuilderProgram`. + */ + EXPERIMENTAL_useLanguageService?: boolean; + /** * When `project` is provided, this controls the non-standard file extensions which will be parsed. * It accepts an array of file extensions, each preceded by a `.`. diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index c5504ba961a0..9086949b6dfd 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -7,6 +7,8 @@ import { createDefaultProgram } from './create-program/createDefaultProgram'; import { createIsolatedProgram } from './create-program/createIsolatedProgram'; import { createProjectProgram } from './create-program/createProjectProgram'; import { createSourceFile } from './create-program/createSourceFile'; +import { getLanguageServiceProgram } from './create-program/getLanguageServiceProgram'; +import { getWatchProgramsForProjects } from './create-program/getWatchProgramsForProjects'; import type { ASTAndProgram, CanonicalPath } from './create-program/shared'; import { createProgramFromConfigFile, @@ -39,15 +41,44 @@ function getProgramAndAST( parseSettings: ParseSettings, shouldProvideParserServices: boolean, ): ASTAndProgram { - return ( - (parseSettings.programs && - useProvidedPrograms(parseSettings.programs, parseSettings)) || - (shouldProvideParserServices && createProjectProgram(parseSettings)) || - (shouldProvideParserServices && - parseSettings.createDefaultProgram && - createDefaultProgram(parseSettings)) || - createIsolatedProgram(parseSettings) - ); + if (parseSettings.programs) { + const providedPrograms = useProvidedPrograms( + parseSettings.programs, + parseSettings, + ); + if (providedPrograms) { + return providedPrograms; + } + } + + if (shouldProvideParserServices) { + if (parseSettings.EXPERIMENTAL_useLanguageService) { + const languageServiceProgram = createProjectProgram( + parseSettings, + getLanguageServiceProgram(parseSettings), + ); + if (languageServiceProgram) { + return languageServiceProgram; + } + } else { + const watchProgram = createProjectProgram( + parseSettings, + getWatchProgramsForProjects(parseSettings), + ); + if (watchProgram) { + return watchProgram; + } + } + + if (parseSettings.createDefaultProgram) { + const defaultProgram = createDefaultProgram(parseSettings); + if (defaultProgram) { + return defaultProgram; + } + } + } + + return createIsolatedProgram(parseSettings); } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/packages/typescript-estree/tests/lib/persistentParse.test.ts b/packages/typescript-estree/tests/lib/persistentParse.test.ts index 63e81d7e260a..52d197a38220 100644 --- a/packages/typescript-estree/tests/lib/persistentParse.test.ts +++ b/packages/typescript-estree/tests/lib/persistentParse.test.ts @@ -2,17 +2,88 @@ import fs from 'fs'; import path from 'path'; import tmp from 'tmp'; -import { clearWatchCaches } from '../../src/create-program/getWatchProgramsForProjects'; +import type { TSESTreeOptions } from '../../src'; +import { AST_NODE_TYPES, simpleTraverse } from '../../src'; +import { clearWatchCaches } from '../../src/create-program/shared'; +import type { ParseAndGenerateServicesResult } from '../../src/parser'; import { parseAndGenerateServices } from '../../src/parser'; const CONTENTS = { - foo: 'console.log("foo")', - bar: 'console.log("bar")', - 'baz/bar': 'console.log("baz bar")', - 'bat/baz/bar': 'console.log("bat/baz/bar")', - number: 'const foo = 1;', - object: '(() => { })();', - string: 'let a: "a" | "b";', + foo: { + code: 'const x = "foo";', + types: [ + [AST_NODE_TYPES.Program, 'any'], + [AST_NODE_TYPES.VariableDeclaration, 'any'], + [AST_NODE_TYPES.VariableDeclarator, '"foo"'], + [AST_NODE_TYPES.Identifier, '"foo"'], + [AST_NODE_TYPES.Literal, '"foo"'], + ], + }, + bar: { + code: 'const x = "bar";', + types: [ + [AST_NODE_TYPES.Program, 'any'], + [AST_NODE_TYPES.VariableDeclaration, 'any'], + [AST_NODE_TYPES.VariableDeclarator, '"bar"'], + [AST_NODE_TYPES.Identifier, '"bar"'], + [AST_NODE_TYPES.Literal, '"bar"'], + ], + }, + 'baz/bar': { + code: 'const x = "baz bar";', + types: [ + [AST_NODE_TYPES.Program, 'any'], + [AST_NODE_TYPES.VariableDeclaration, 'any'], + [AST_NODE_TYPES.VariableDeclarator, '"baz bar"'], + [AST_NODE_TYPES.Identifier, '"baz bar"'], + [AST_NODE_TYPES.Literal, '"baz bar"'], + ], + }, + 'bat/baz/bar': { + code: 'const x = "bat/baz/bar";', + types: [ + [AST_NODE_TYPES.Program, 'any'], + [AST_NODE_TYPES.VariableDeclaration, 'any'], + [AST_NODE_TYPES.VariableDeclarator, '"bat/baz/bar"'], + [AST_NODE_TYPES.Identifier, '"bat/baz/bar"'], + [AST_NODE_TYPES.Literal, '"bat/baz/bar"'], + ], + }, + number: { + code: 'const foo = 1;', + types: [ + [AST_NODE_TYPES.Program, 'any'], + [AST_NODE_TYPES.VariableDeclaration, 'any'], + [AST_NODE_TYPES.VariableDeclarator, '1'], + [AST_NODE_TYPES.Identifier, '1'], + [AST_NODE_TYPES.Literal, '1'], + ], + }, + object: { + code: '(() => { })();', + types: [ + [AST_NODE_TYPES.Program, 'any'], + [AST_NODE_TYPES.ExpressionStatement, 'any'], + [AST_NODE_TYPES.CallExpression, 'void'], + [AST_NODE_TYPES.ArrowFunctionExpression, '() => void'], + [AST_NODE_TYPES.BlockStatement, 'any'], + ], + }, + string: { + code: 'let a: "a" | "b";', + types: [ + [AST_NODE_TYPES.Program, 'any'], + [AST_NODE_TYPES.VariableDeclaration, 'any'], + [AST_NODE_TYPES.VariableDeclarator, '"a" | "b"'], + [AST_NODE_TYPES.Identifier, '"a" | "b"'], + [AST_NODE_TYPES.TSTypeAnnotation, 'any'], + [AST_NODE_TYPES.TSUnionType, '"a" | "b"'], + [AST_NODE_TYPES.TSLiteralType, '"a"'], + [AST_NODE_TYPES.Literal, 'any'], + [AST_NODE_TYPES.TSLiteralType, '"b"'], + [AST_NODE_TYPES.Literal, 'any'], + ], + }, }; const cwdCopy = process.cwd(); @@ -20,7 +91,8 @@ const tmpDirs = new Set(); afterEach(() => { // stop watching the files and folders clearWatchCaches(); - +}); +afterAll(() => { // clean up the temporary files and folders tmpDirs.forEach(t => t.removeCallback()); tmpDirs.clear(); @@ -29,201 +101,370 @@ afterEach(() => { process.chdir(cwdCopy); }); -function writeTSConfig(dirName: string, config: Record): void { - fs.writeFileSync(path.join(dirName, 'tsconfig.json'), JSON.stringify(config)); -} -function writeFile(dirName: string, file: keyof typeof CONTENTS): void { - fs.writeFileSync(path.join(dirName, 'src', `${file}.ts`), CONTENTS[file]); -} -function renameFile(dirName: string, src: 'bar', dest: 'baz/bar'): void { - fs.renameSync( - path.join(dirName, 'src', `${src}.ts`), - path.join(dirName, 'src', `${dest}.ts`), - ); -} - -function createTmpDir(): tmp.DirResult { - const tmpDir = tmp.dirSync({ - keep: false, - unsafeCleanup: true, - }); - tmpDirs.add(tmpDir); - return tmpDir; -} -function setup(tsconfig: Record, writeBar = true): string { - const tmpDir = createTmpDir(); - - writeTSConfig(tmpDir.name, tsconfig); - - fs.mkdirSync(path.join(tmpDir.name, 'src')); - fs.mkdirSync(path.join(tmpDir.name, 'src', 'baz')); - writeFile(tmpDir.name, 'foo'); - writeBar && writeFile(tmpDir.name, 'bar'); - - return tmpDir.name; -} - -function parseFile( - filename: keyof typeof CONTENTS, - tmpDir: string, - relative?: boolean, - ignoreTsconfigRootDir?: boolean, -): void { - parseAndGenerateServices(CONTENTS[filename], { - project: './tsconfig.json', - tsconfigRootDir: ignoreTsconfigRootDir ? undefined : tmpDir, - filePath: relative - ? path.join('src', `${filename}.ts`) - : path.join(tmpDir, 'src', `${filename}.ts`), - }); -} - -function existsSync(filename: keyof typeof CONTENTS, tmpDir = ''): boolean { - return fs.existsSync(path.join(tmpDir, 'src', `${filename}.ts`)); -} - -function baseTests( - tsConfigExcludeBar: Record, - tsConfigIncludeAll: Record, -): void { - it('parses both files successfully when included', () => { - const PROJECT_DIR = setup(tsConfigIncludeAll); - - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); - }); +function parserTests(extraOptions: TSESTreeOptions): void { + function writeTSConfig( + dirName: string, + config: Record, + ): void { + fs.writeFileSync( + path.join(dirName, 'tsconfig.json'), + JSON.stringify(config), + ); + } + function writeFile(dirName: string, file: keyof typeof CONTENTS): void { + fs.writeFileSync( + path.join(dirName, 'src', `${file}.ts`), + CONTENTS[file].code, + ); + } + function renameFile(dirName: string, src: 'bar', dest: 'baz/bar'): void { + fs.renameSync( + path.join(dirName, 'src', `${src}.ts`), + path.join(dirName, 'src', `${dest}.ts`), + ); + } + + function createTmpDir(): tmp.DirResult { + const tmpDir = tmp.dirSync({ + keep: false, + unsafeCleanup: true, + }); + tmpDirs.add(tmpDir); + return tmpDir; + } + function setup(tsconfig: Record, writeBar = true): string { + const tmpDir = createTmpDir(); + + writeTSConfig(tmpDir.name, tsconfig); + + fs.mkdirSync(path.join(tmpDir.name, 'src')); + fs.mkdirSync(path.join(tmpDir.name, 'src', 'baz')); + writeFile(tmpDir.name, 'foo'); + writeBar && writeFile(tmpDir.name, 'bar'); + + return tmpDir.name; + } + + function parseFile({ + filename, + ignoreTsconfigRootDir, + relative, + shouldThrowError, + tmpDir, + }: { + filename: keyof typeof CONTENTS; + ignoreTsconfigRootDir?: boolean; + relative?: boolean; + shouldThrowError?: boolean; + tmpDir: string; + }): void { + describe(filename, () => { + // eslint-disable-next-line @typescript-eslint/ban-types + const result = ((): ParseAndGenerateServicesResult<{}> | Error => { + try { + return parseAndGenerateServices(CONTENTS[filename].code, { + ...extraOptions, + project: './tsconfig.json', + tsconfigRootDir: ignoreTsconfigRootDir ? undefined : tmpDir, + filePath: relative + ? path.join('src', `${filename}.ts`) + : path.join(tmpDir, 'src', `${filename}.ts`), + }); + } catch (ex) { + return ex as Error; + } + })(); + if (shouldThrowError === true) { + it('should throw', () => { + expect(result).toBeInstanceOf(Error); + const message = (result as Error).message; + expect(message).toMatch( + new RegExp(`/src/${filename}`), + ); + expect(message).toMatch(/TSConfig does not include this file/); + }); + return; + } else { + it('should not throw', () => { + expect(result).not.toBeInstanceOf(Error); + }); + } + if (result instanceof Error) { + // not possible to reach + return; + } + + it('should have full type information available for nodes', () => { + expect(result.services.hasFullTypeInformation).toBeTruthy(); + const checker = result.services.program.getTypeChecker(); + const types: Array<[AST_NODE_TYPES, string]> = []; + simpleTraverse(result.ast, { + enter(node) { + const type = checker.getTypeAtLocation( + result.services.esTreeNodeToTSNodeMap.get(node), + ); + types.push([node.type, checker.typeToString(type)]); + }, + }); + expect(types).toStrictEqual(CONTENTS[filename].types); + }); + }); + } + + function existsSync(filename: keyof typeof CONTENTS, tmpDir = ''): boolean { + return fs.existsSync(path.join(tmpDir, 'src', `${filename}.ts`)); + } + + function baseTests( + tsConfigExcludeBar: Record, + tsConfigIncludeAll: Record, + ): void { + describe('parses both files successfully when included', () => { + const PROJECT_DIR = setup(tsConfigIncludeAll); + + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + parseFile({ + filename: 'bar', + tmpDir: PROJECT_DIR, + }); + }); - it('parses included files, and throws on excluded files', () => { - const PROJECT_DIR = setup(tsConfigExcludeBar); + describe('parses included files, and throws on excluded files', () => { + const PROJECT_DIR = setup(tsConfigExcludeBar); - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); - }); + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + parseFile({ + shouldThrowError: true, + filename: 'bar', + tmpDir: PROJECT_DIR, + }); + }); - it('allows parsing of new files', () => { - const PROJECT_DIR = setup(tsConfigIncludeAll, false); + describe('allows parsing of new files', () => { + const PROJECT_DIR = setup(tsConfigIncludeAll, false); - // parse once to: assert the config as correct, and to make sure the program is setup - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - // bar should throw because it doesn't exist yet - expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); + // parse once to: assert the config as correct, and to make sure the program is setup + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + // bar should throw because it doesn't exist yet + parseFile({ + shouldThrowError: true, + filename: 'bar', + tmpDir: PROJECT_DIR, + }); - // write a new file and attempt to parse it - writeFile(PROJECT_DIR, 'bar'); + // write a new file and attempt to parse it + writeFile(PROJECT_DIR, 'bar'); - // both files should parse fine now - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); - }); + // both files should parse fine now + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + parseFile({ + filename: 'bar', + tmpDir: PROJECT_DIR, + }); + }); - it('allows parsing of deeply nested new files', () => { - const PROJECT_DIR = setup(tsConfigIncludeAll, false); - const bazSlashBar = 'baz/bar' as const; + describe('allows parsing of deeply nested new files', () => { + const PROJECT_DIR = setup(tsConfigIncludeAll, false); + const bazSlashBar = 'baz/bar' as const; - // parse once to: assert the config as correct, and to make sure the program is setup - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - // bar should throw because it doesn't exist yet - expect(() => parseFile(bazSlashBar, PROJECT_DIR)).toThrow(); + // parse once to: assert the config as correct, and to make sure the program is setup + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + // bar should throw because it doesn't exist yet + parseFile({ + shouldThrowError: true, + filename: bazSlashBar, + tmpDir: PROJECT_DIR, + }); - // write a new file and attempt to parse it - writeFile(PROJECT_DIR, bazSlashBar); + // write a new file and attempt to parse it + writeFile(PROJECT_DIR, bazSlashBar); - // both files should parse fine now - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); - }); + // both files should parse fine now + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + parseFile({ + filename: bazSlashBar, + tmpDir: PROJECT_DIR, + }); + }); - it('allows parsing of deeply nested new files in new folder', () => { - const PROJECT_DIR = setup(tsConfigIncludeAll); + describe('allows parsing of deeply nested new files in new folder', () => { + const PROJECT_DIR = setup(tsConfigIncludeAll); - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); - // Create deep folder structure after first parse (this is important step) - // context: https://github.com/typescript-eslint/typescript-eslint/issues/1394 - fs.mkdirSync(path.join(PROJECT_DIR, 'src', 'bat')); - fs.mkdirSync(path.join(PROJECT_DIR, 'src', 'bat', 'baz')); + // Create deep folder structure after first parse (this is important step) + // context: https://github.com/typescript-eslint/typescript-eslint/issues/1394 + fs.mkdirSync(path.join(PROJECT_DIR, 'src', 'bat')); + fs.mkdirSync(path.join(PROJECT_DIR, 'src', 'bat', 'baz')); - const bazSlashBar = 'bat/baz/bar' as const; + const bazSlashBar = 'bat/baz/bar' as const; - // write a new file and attempt to parse it - writeFile(PROJECT_DIR, bazSlashBar); + // write a new file and attempt to parse it + writeFile(PROJECT_DIR, bazSlashBar); - expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); - }); + parseFile({ + filename: bazSlashBar, + tmpDir: PROJECT_DIR, + }); + }); - it('allows renaming of files', () => { - const PROJECT_DIR = setup(tsConfigIncludeAll, true); - const bazSlashBar = 'baz/bar' as const; + describe('allows renaming of files', () => { + const PROJECT_DIR = setup(tsConfigIncludeAll, true); + const bazSlashBar = 'baz/bar' as const; - // parse once to: assert the config as correct, and to make sure the program is setup - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - // bar should throw because it doesn't exist yet - expect(() => parseFile(bazSlashBar, PROJECT_DIR)).toThrow(); + // parse once to: assert the config as correct, and to make sure the program is setup + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + // bar should throw because it doesn't exist yet + parseFile({ + shouldThrowError: true, + filename: bazSlashBar, + tmpDir: PROJECT_DIR, + }); - // write a new file and attempt to parse it - renameFile(PROJECT_DIR, 'bar', bazSlashBar); + // write a new file and attempt to parse it + renameFile(PROJECT_DIR, 'bar', bazSlashBar); - // both files should parse fine now - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); - }); + // both files should parse fine now + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + parseFile({ + filename: bazSlashBar, + tmpDir: PROJECT_DIR, + }); + }); - it('reacts to changes in the tsconfig', () => { - const PROJECT_DIR = setup(tsConfigExcludeBar); + describe('reacts to changes in the tsconfig', () => { + const PROJECT_DIR = setup(tsConfigExcludeBar); - // parse once to: assert the config as correct, and to make sure the program is setup - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); + // parse once to: assert the config as correct, and to make sure the program is setup + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + parseFile({ + shouldThrowError: true, + filename: 'bar', + tmpDir: PROJECT_DIR, + }); - // change the config file so it now includes all files - writeTSConfig(PROJECT_DIR, tsConfigIncludeAll); + // change the config file so it now includes all files + writeTSConfig(PROJECT_DIR, tsConfigIncludeAll); - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); - }); + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + parseFile({ + filename: 'bar', + tmpDir: PROJECT_DIR, + }); + }); - it('should work with relative paths', () => { - const PROJECT_DIR = setup(tsConfigIncludeAll, false); + describe('should work with relative paths', () => { + const PROJECT_DIR = setup(tsConfigIncludeAll, false); - // parse once to: assert the config as correct, and to make sure the program is setup - expect(() => parseFile('foo', PROJECT_DIR, true)).not.toThrow(); - // bar should throw because it doesn't exist yet - expect(() => parseFile('bar', PROJECT_DIR, true)).toThrow(); + // parse once to: assert the config as correct, and to make sure the program is setup + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + relative: true, + }); + // bar should throw because it doesn't exist yet + parseFile({ + shouldThrowError: true, + filename: 'bar', + tmpDir: PROJECT_DIR, + relative: true, + }); - // write a new file and attempt to parse it - writeFile(PROJECT_DIR, 'bar'); + // write a new file and attempt to parse it + writeFile(PROJECT_DIR, 'bar'); - // make sure that file is correctly created - expect(existsSync('bar', PROJECT_DIR)).toBe(true); + // make sure that file is correctly created + expect(existsSync('bar', PROJECT_DIR)).toBe(true); - // both files should parse fine now - expect(() => parseFile('foo', PROJECT_DIR, true)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR, true)).not.toThrow(); - }); + // both files should parse fine now + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + relative: true, + }); + parseFile({ + filename: 'bar', + tmpDir: PROJECT_DIR, + relative: true, + }); + }); - it('should work with relative paths without tsconfig root', () => { - const PROJECT_DIR = setup(tsConfigIncludeAll, false); - process.chdir(PROJECT_DIR); + describe('should work with relative paths without tsconfig root', () => { + const PROJECT_DIR = setup(tsConfigIncludeAll, false); + process.chdir(PROJECT_DIR); - // parse once to: assert the config as correct, and to make sure the program is setup - expect(() => parseFile('foo', PROJECT_DIR, true, true)).not.toThrow(); - // bar should throw because it doesn't exist yet - expect(() => parseFile('bar', PROJECT_DIR, true, true)).toThrow(); + // parse once to: assert the config as correct, and to make sure the program is setup + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + relative: true, + ignoreTsconfigRootDir: true, + }); + // bar should throw because it doesn't exist yet + parseFile({ + shouldThrowError: true, + filename: 'bar', + tmpDir: PROJECT_DIR, + relative: true, + ignoreTsconfigRootDir: true, + }); - // write a new file and attempt to parse it - writeFile(PROJECT_DIR, 'bar'); + // write a new file and attempt to parse it + writeFile(PROJECT_DIR, 'bar'); - // make sure that file is correctly created - expect(existsSync('bar')).toBe(true); - expect(existsSync('bar', PROJECT_DIR)).toBe(true); + // make sure that file is correctly created + expect(existsSync('bar')).toBe(true); + expect(existsSync('bar', PROJECT_DIR)).toBe(true); - // both files should parse fine now - expect(() => parseFile('foo', PROJECT_DIR, true, true)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR, true, true)).not.toThrow(); - }); -} + // both files should parse fine now + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + relative: true, + ignoreTsconfigRootDir: true, + }); + parseFile({ + filename: 'bar', + tmpDir: PROJECT_DIR, + relative: true, + ignoreTsconfigRootDir: true, + }); + }); + } -describe('persistent parse', () => { describe('includes not ending in a slash', () => { const tsConfigExcludeBar = { include: ['src'], @@ -265,33 +506,59 @@ describe('persistent parse', () => { baseTests(tsConfigExcludeBar, tsConfigIncludeAll); - it('handles tsconfigs with no includes/excludes (single level)', () => { + describe('handles tsconfigs with no includes/excludes (single level)', () => { const PROJECT_DIR = setup({}, false); // parse once to: assert the config as correct, and to make sure the program is setup - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + parseFile({ + shouldThrowError: true, + filename: 'bar', + tmpDir: PROJECT_DIR, + }); // write a new file and attempt to parse it writeFile(PROJECT_DIR, 'bar'); - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + parseFile({ + filename: 'bar', + tmpDir: PROJECT_DIR, + }); }); - it('handles tsconfigs with no includes/excludes (nested)', () => { + describe('handles tsconfigs with no includes/excludes (nested)', () => { const PROJECT_DIR = setup({}, false); const bazSlashBar = 'baz/bar' as const; // parse once to: assert the config as correct, and to make sure the program is setup - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile(bazSlashBar, PROJECT_DIR)).toThrow(); + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + parseFile({ + shouldThrowError: true, + filename: bazSlashBar, + tmpDir: PROJECT_DIR, + }); // write a new file and attempt to parse it writeFile(PROJECT_DIR, bazSlashBar); - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); + parseFile({ + filename: 'foo', + tmpDir: PROJECT_DIR, + }); + parseFile({ + filename: bazSlashBar, + tmpDir: PROJECT_DIR, + }); }); }); @@ -331,13 +598,25 @@ describe('persistent parse', () => { const testNames = ['object', 'number', 'string', 'foo'] as const; for (const name of testNames) { - it(`first parse of ${name} should not throw`, () => { + describe(`first parse of ${name} should not throw`, () => { const PROJECT_DIR = setup(tsConfigIncludeAll); writeFile(PROJECT_DIR, name); - expect(() => parseFile(name, PROJECT_DIR)).not.toThrow(); + parseFile({ + filename: name, + tmpDir: PROJECT_DIR, + }); }); } }); } }); +} + +describe('persistent parse', () => { + describe('Builder Program', () => { + parserTests({ EXPERIMENTAL_useLanguageService: false }); + }); + describe('Language Service', () => { + parserTests({ EXPERIMENTAL_useLanguageService: true }); + }); }); diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index 3ebe689185e1..f4dad7562932 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -3,7 +3,7 @@ import glob from 'glob'; import * as path from 'path'; import * as ts from 'typescript'; -import { clearWatchCaches } from '../../src/create-program/getWatchProgramsForProjects'; +import { clearWatchCaches } from '../../src/create-program/shared'; import { createProgramFromConfigFile as createProgram } from '../../src/create-program/useProvidedPrograms'; import type { ParseAndGenerateServicesResult } from '../../src/parser'; import { parseAndGenerateServices } from '../../src/parser'; diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index f077f3786ee1..5263d7cdfd79 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -1,3 +1,4 @@ +import type { CanonicalPath } from '@site/../typescript-estree/dist/create-program/shared'; import type { ParseSettings } from '@typescript-eslint/typescript-estree/dist/parseSettings'; export const parseSettings: ParseSettings = { @@ -8,7 +9,7 @@ export const parseSettings: ParseSettings = { debugLevel: new Set(), errorOnUnknownASTType: false, extraFileExtensions: [], - filePath: '', + filePath: '' as CanonicalPath, jsx: false, loc: true, // eslint-disable-next-line no-console @@ -17,9 +18,10 @@ export const parseSettings: ParseSettings = { projects: [], range: true, tokens: [], - tsconfigRootDir: '/', + tsconfigRootDir: '/' as CanonicalPath, errorOnTypeScriptSyntacticAndSemanticIssues: false, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: false, + EXPERIMENTAL_useLanguageService: false, singleRun: false, programs: null, moduleResolver: '', pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy