Skip to content

Commit dcb0fa7

Browse files
authored
Incremental testing for program structure and resolutions and fixes (#55814)
1 parent 83f02a4 commit dcb0fa7

23 files changed

+2082
-169
lines changed

src/compiler/program.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1109,7 +1109,8 @@ export function getInferredLibraryNameResolveFrom(options: CompilerOptions, curr
11091109
return combinePaths(containingDirectory, `__lib_node_modules_lookup_${libFileName}__.ts`);
11101110
}
11111111

1112-
function getLibraryNameFromLibFileName(libFileName: string) {
1112+
/** @internal */
1113+
export function getLibraryNameFromLibFileName(libFileName: string) {
11131114
// Support resolving to lib.dom.d.ts -> @typescript/lib-dom, and
11141115
// lib.dom.iterable.d.ts -> @typescript/lib-dom/iterable
11151116
// lib.es2015.symbol.wellknown.d.ts -> @typescript/lib-es2015/symbol-wellknown

src/compiler/resolutionCache.ts

Lines changed: 197 additions & 121 deletions
Large diffs are not rendered by default.

src/compiler/watchPublic.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
perfLogger,
6666
PollingInterval,
6767
ProjectReference,
68+
ResolutionCache,
6869
ResolutionCacheHost,
6970
ResolutionMode,
7071
ResolvedModule,
@@ -341,6 +342,8 @@ export interface Watch<T> {
341342
getCurrentProgram(): T;
342343
/** Closes the watch */
343344
close(): void;
345+
/** @internal */
346+
getResolutionCache(): ResolutionCache;
344347
}
345348

346349
/**
@@ -552,8 +555,8 @@ export function createWatchProgram<T extends BuilderProgram>(host: WatchCompiler
552555
if (configFileName) updateExtendedConfigFilesWatches(toPath(configFileName), compilerOptions, watchOptions, WatchType.ExtendedConfigFile);
553556

554557
return configFileName ?
555-
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, close } :
556-
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, updateRootFileNames, close };
558+
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, close, getResolutionCache } :
559+
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, updateRootFileNames, close, getResolutionCache };
557560

558561
function close() {
559562
clearInvalidateResolutionsOfFailedLookupLocations();
@@ -593,6 +596,10 @@ export function createWatchProgram<T extends BuilderProgram>(host: WatchCompiler
593596
}
594597
}
595598

599+
function getResolutionCache() {
600+
return resolutionCache;
601+
}
602+
596603
function getCurrentBuilderProgram() {
597604
return builderProgram;
598605
}

src/harness/incrementalUtils.ts

Lines changed: 416 additions & 1 deletion
Large diffs are not rendered by default.

src/server/editorServices.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,7 @@ export class ProjectService {
999999
private currentPluginEnablementPromise?: Promise<void>;
10001000

10011001
/** @internal */ verifyDocumentRegistry = noop;
1002+
/** @internal */ verifyProgram: (project: Project) => void = noop;
10021003

10031004
readonly jsDocParsingMode: JSDocParsingMode | undefined;
10041005

src/server/project.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,6 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
539539
// If files are listed explicitly or allowJs is specified, allow all extensions
540540
this.compilerOptions.allowNonTsExtensions = true;
541541
}
542-
543542
switch (projectService.serverMode) {
544543
case LanguageServiceMode.Semantic:
545544
this.languageServiceEnabled = true;
@@ -1541,13 +1540,13 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
15411540
if (!newFile || (f.resolvedPath === f.path && newFile.resolvedPath !== f.path)) {
15421541
// new program does not contain this file - detach it from the project
15431542
// - remove resolutions only if the new program doesnt contain source file by the path (not resolvedPath since path is used for resolution)
1544-
this.detachScriptInfoFromProject(f.fileName, !!this.program.getSourceFileByPath(f.path));
1543+
this.detachScriptInfoFromProject(f.fileName, !!this.program.getSourceFileByPath(f.path), /*syncDirWatcherRemove*/ true);
15451544
}
15461545
}
15471546

15481547
oldProgram.forEachResolvedProjectReference(resolvedProjectReference => {
15491548
if (!this.program!.getResolvedProjectReferenceByPath(resolvedProjectReference.sourceFile.path)) {
1550-
this.detachScriptInfoFromProject(resolvedProjectReference.sourceFile.fileName);
1549+
this.detachScriptInfoFromProject(resolvedProjectReference.sourceFile.fileName, /*noRemoveResolution*/ undefined, /*syncDirWatcherRemove*/ true);
15511550
}
15521551
});
15531552
}
@@ -1603,6 +1602,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
16031602
}
16041603
}
16051604

1605+
this.projectService.verifyProgram(this);
16061606
if (this.exportMapCache && !this.exportMapCache.isEmpty()) {
16071607
this.exportMapCache.releaseSymbols();
16081608
if (this.hasAddedorRemovedFiles || oldProgram && !this.program!.structureIsReused) {
@@ -1671,12 +1671,12 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
16711671
this.projectService.sendPerformanceEvent(kind, durationMs);
16721672
}
16731673

1674-
private detachScriptInfoFromProject(uncheckedFileName: string, noRemoveResolution?: boolean) {
1674+
private detachScriptInfoFromProject(uncheckedFileName: string, noRemoveResolution?: boolean, syncDirWatcherRemove?: boolean) {
16751675
const scriptInfoToDetach = this.projectService.getScriptInfo(uncheckedFileName);
16761676
if (scriptInfoToDetach) {
16771677
scriptInfoToDetach.detachFromProject(this);
16781678
if (!noRemoveResolution) {
1679-
this.resolutionCache.removeResolutionsOfFile(scriptInfoToDetach.path);
1679+
this.resolutionCache.removeResolutionsOfFile(scriptInfoToDetach.path, syncDirWatcherRemove);
16801680
}
16811681
}
16821682
}
@@ -2516,16 +2516,17 @@ export class AutoImportProviderProject extends Project {
25162516
);
25172517
if (entrypoints) {
25182518
const real = host.realpath?.(packageJson.packageDirectory);
2519-
const isSymlink = real && real !== packageJson.packageDirectory;
2519+
const realPath = real ? hostProject.toPath(real) : undefined;
2520+
const isSymlink = realPath && realPath !== hostProject.toPath(packageJson.packageDirectory);
25202521
if (isSymlink) {
25212522
symlinkCache.setSymlinkedDirectory(packageJson.packageDirectory, {
2522-
real,
2523-
realPath: hostProject.toPath(real),
2523+
real: real!,
2524+
realPath,
25242525
});
25252526
}
25262527

25272528
return mapDefined(entrypoints, entrypoint => {
2528-
const resolvedFileName = isSymlink ? entrypoint.replace(packageJson.packageDirectory, real) : entrypoint;
2529+
const resolvedFileName = isSymlink ? entrypoint.replace(packageJson.packageDirectory, real!) : entrypoint;
25292530
if (!program.getSourceFile(resolvedFileName) && !(isSymlink && program.getSourceFile(entrypoint))) {
25302531
return resolvedFileName;
25312532
}

src/testRunner/unittests/helpers/baseline.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,10 @@ export function commandLineCallbacks(
4545
};
4646
}
4747

48-
export function baselinePrograms(baseline: string[], getPrograms: () => readonly CommandLineProgram[], oldPrograms: readonly (CommandLineProgram | undefined)[], baselineDependencies: boolean | undefined) {
49-
const programs = getPrograms();
48+
export function baselinePrograms(baseline: string[], programs: readonly CommandLineProgram[], oldPrograms: readonly (CommandLineProgram | undefined)[], baselineDependencies: boolean | undefined) {
5049
for (let i = 0; i < programs.length; i++) {
5150
baselineProgram(baseline, programs[i], oldPrograms[i], baselineDependencies);
5251
}
53-
return programs;
5452
}
5553

5654
function baselineProgram(baseline: string[], [program, builderProgram]: CommandLineProgram, oldProgram: CommandLineProgram | undefined, baselineDependencies: boolean | undefined) {

src/testRunner/unittests/helpers/tsc.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,11 @@ export function testTscCompile(input: TestTscCompile) {
156156

157157
function additionalBaseline(sys: TscCompileSystem) {
158158
const { baselineSourceMap, baselineReadFileCalls, baselinePrograms: shouldBaselinePrograms, baselineDependencies } = input;
159-
if (input.computeDtsSignatures) storeDtsSignatures(sys, getPrograms!());
159+
const programs = getPrograms!();
160+
if (input.computeDtsSignatures) storeDtsSignatures(sys, programs);
160161
if (shouldBaselinePrograms) {
161162
const baseline: string[] = [];
162-
baselinePrograms(baseline, getPrograms!, ts.emptyArray, baselineDependencies);
163+
baselinePrograms(baseline, programs, ts.emptyArray, baselineDependencies);
163164
sys.write(baseline.join("\n"));
164165
}
165166
if (baselineReadFileCalls) {

src/testRunner/unittests/helpers/tscWatch.ts

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import {
2+
verifyProgramStructure,
3+
verifyResolutionCache,
4+
} from "../../../harness/incrementalUtils";
15
import {
26
patchHostForBuildInfoReadWrite,
37
} from "../../_namespaces/fakes";
@@ -38,6 +42,9 @@ export interface TscWatchCompileChange<T extends ts.BuilderProgram = ts.EmitAndS
3842
programs: readonly CommandLineProgram[],
3943
watchOrSolution: WatchOrSolution<T>,
4044
) => void;
45+
// TODO:: sheetal: Needing these fields are technically issues that need to be fixed later
46+
symlinksNotReflected?: readonly string[];
47+
skipStructureCheck?: true;
4148
}
4249
export interface TscWatchCheckOptions {
4350
baselineSourceMap?: boolean;
@@ -214,6 +221,7 @@ export interface RunWatchBaseline<T extends ts.BuilderProgram> extends BaselineB
214221
sys: TscWatchSystem;
215222
getPrograms: () => readonly CommandLineProgram[];
216223
watchOrSolution: WatchOrSolution<T>;
224+
useSourceOfProjectReferenceRedirect?: () => boolean;
217225
}
218226
export function runWatchBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanticDiagnosticsBuilderProgram>({
219227
scenario,
@@ -227,6 +235,7 @@ export function runWatchBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanti
227235
baselineDependencies,
228236
edits,
229237
watchOrSolution,
238+
useSourceOfProjectReferenceRedirect,
230239
}: RunWatchBaseline<T>) {
231240
baseline.push(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}`);
232241
let programs = watchBaseline({
@@ -240,7 +249,7 @@ export function runWatchBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanti
240249
});
241250

242251
if (edits) {
243-
for (const { caption, edit, timeouts } of edits) {
252+
for (const { caption, edit, timeouts, symlinksNotReflected, skipStructureCheck } of edits) {
244253
oldSnap = applyEdit(sys, baseline, edit, caption);
245254
timeouts(sys, programs, watchOrSolution);
246255
programs = watchBaseline({
@@ -251,6 +260,10 @@ export function runWatchBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanti
251260
oldSnap,
252261
baselineSourceMap,
253262
baselineDependencies,
263+
caption,
264+
resolutionCache: !skipStructureCheck ? (watchOrSolution as ts.WatchOfConfigFile<T> | undefined)?.getResolutionCache?.() : undefined,
265+
useSourceOfProjectReferenceRedirect,
266+
symlinksNotReflected,
254267
});
255268
}
256269
}
@@ -268,20 +281,94 @@ export function isWatch(commandLineArgs: readonly string[]) {
268281
export interface WatchBaseline extends BaselineBase, TscWatchCheckOptions {
269282
oldPrograms: readonly (CommandLineProgram | undefined)[];
270283
getPrograms: () => readonly CommandLineProgram[];
284+
caption?: string;
285+
resolutionCache?: ts.ResolutionCache;
286+
useSourceOfProjectReferenceRedirect?: () => boolean;
287+
symlinksNotReflected?: readonly string[];
271288
}
272-
export function watchBaseline({ baseline, getPrograms, oldPrograms, sys, oldSnap, baselineSourceMap, baselineDependencies }: WatchBaseline) {
289+
export function watchBaseline({
290+
baseline,
291+
getPrograms,
292+
oldPrograms,
293+
sys,
294+
oldSnap,
295+
baselineSourceMap,
296+
baselineDependencies,
297+
caption,
298+
resolutionCache,
299+
useSourceOfProjectReferenceRedirect,
300+
symlinksNotReflected,
301+
}: WatchBaseline) {
273302
if (baselineSourceMap) generateSourceMapBaselineFiles(sys);
274303
sys.serializeOutput(baseline);
275-
const programs = baselinePrograms(baseline, getPrograms, oldPrograms, baselineDependencies);
304+
const programs = getPrograms();
305+
baselinePrograms(baseline, programs, oldPrograms, baselineDependencies);
276306
sys.serializeWatches(baseline);
277307
baseline.push(`exitCode:: ExitStatus.${ts.ExitStatus[sys.exitCode as ts.ExitStatus]}`, "");
278308
sys.diff(baseline, oldSnap);
279309
sys.writtenFiles.forEach((value, key) => {
280310
assert.equal(value, 1, `Expected to write file ${key} only once`);
281311
});
312+
// Verify program structure and resolution cache when incremental edit with tsc --watch (without build mode)
313+
if (resolutionCache && programs.length) {
314+
ts.Debug.assert(programs.length === 1);
315+
verifyProgramStructureAndResolutionCache(caption!, sys, programs[0][0], resolutionCache, useSourceOfProjectReferenceRedirect, symlinksNotReflected);
316+
}
282317
sys.writtenFiles.clear();
283318
return programs;
284319
}
320+
function verifyProgramStructureAndResolutionCache(
321+
caption: string,
322+
sys: TscWatchSystem,
323+
program: ts.Program,
324+
resolutionCache: ts.ResolutionCache,
325+
useSourceOfProjectReferenceRedirect?: () => boolean,
326+
symlinksNotReflected?: readonly string[],
327+
) {
328+
const options = program.getCompilerOptions();
329+
const compilerHost = ts.createCompilerHostWorker(options, /*setParentNodes*/ undefined, sys);
330+
compilerHost.trace = ts.noop;
331+
compilerHost.writeFile = ts.notImplemented;
332+
compilerHost.useSourceOfProjectReferenceRedirect = useSourceOfProjectReferenceRedirect;
333+
const readFile = compilerHost.readFile;
334+
compilerHost.readFile = fileName => {
335+
const text = readFile.call(compilerHost, fileName);
336+
if (!ts.contains(symlinksNotReflected, fileName)) return text;
337+
// Handle symlinks that dont reflect the watch change
338+
ts.Debug.assert(sys.toPath(sys.realpath(fileName)) !== sys.toPath(fileName));
339+
const file = program.getSourceFile(fileName)!;
340+
ts.Debug.assert(file.text !== text);
341+
return file.text;
342+
};
343+
verifyProgramStructure(
344+
ts.createProgram({
345+
rootNames: program.getRootFileNames(),
346+
options,
347+
projectReferences: program.getProjectReferences(),
348+
host: compilerHost,
349+
}),
350+
program,
351+
caption,
352+
);
353+
verifyResolutionCache(resolutionCache, program, {
354+
...compilerHost,
355+
356+
getCompilerHost: () => compilerHost,
357+
toPath: fileName => sys.toPath(fileName),
358+
getCompilationSettings: () => options,
359+
fileIsOpen: ts.returnFalse,
360+
getCurrentProgram: () => program,
361+
362+
watchDirectoryOfFailedLookupLocation: ts.returnNoopFileWatcher,
363+
watchAffectingFileLocation: ts.returnNoopFileWatcher,
364+
onInvalidatedResolution: ts.noop,
365+
watchTypeRootsDirectory: ts.returnNoopFileWatcher,
366+
onChangedAutomaticTypeDirectiveNames: ts.noop,
367+
scheduleInvalidateResolutionsOfFailedLookupLocations: ts.noop,
368+
getCachedDirectoryStructureHost: ts.returnUndefined,
369+
writeLog: ts.noop,
370+
}, caption);
371+
}
285372
export interface VerifyTscWatch extends TscWatchCompile {
286373
baselineIncremental?: boolean;
287374
}

src/testRunner/unittests/helpers/tsserver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ function patchHostTimeouts(
489489
export interface TestSessionOptions extends ts.server.SessionOptions {
490490
logger: Logger;
491491
allowNonBaseliningLogger?: boolean;
492+
disableAutomaticTypingAcquisition?: boolean;
492493
}
493494

494495
export type TestSessionRequest<T extends ts.server.protocol.Request> = Pick<T, "command" | "arguments">;
@@ -543,7 +544,7 @@ export class TestSession extends ts.server.Session {
543544

544545
export function createSession(host: TestServerHost, opts: Partial<TestSessionOptions> = {}) {
545546
const logger = opts.logger || createHasErrorMessageLogger();
546-
if (opts.typingsInstaller === undefined) {
547+
if (!opts.disableAutomaticTypingAcquisition && opts.typingsInstaller === undefined) {
547548
opts.typingsInstaller = new TestTypingsInstaller(host.getHostSpecificPath("/a/data/"), /*throttleLimit*/ 5, host, logger);
548549
}
549550

@@ -556,7 +557,6 @@ export function createSession(host: TestServerHost, opts: Partial<TestSessionOpt
556557
cancellationToken: ts.server.nullCancellationToken,
557558
useSingleInferredProject: false,
558559
useInferredProjectPerProjectRoot: false,
559-
typingsInstaller: undefined!, // TODO: GH#18217
560560
byteLength: Buffer.byteLength,
561561
hrtime: process.hrtime,
562562
logger,

0 commit comments

Comments
 (0)