Skip to content

Commit 883f2bc

Browse files
sheetalkamatsnovader
authored andcommitted
Incremental testing for program structure and resolutions and fixes (microsoft#55814)
1 parent cc872ed commit 883f2bc

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,
@@ -342,6 +343,8 @@ export interface Watch<T> {
342343
getCurrentProgram(): T;
343344
/** Closes the watch */
344345
close(): void;
346+
/** @internal */
347+
getResolutionCache(): ResolutionCache;
345348
}
346349

347350
/**
@@ -559,8 +562,8 @@ export function createWatchProgram<T extends BuilderProgram>(host: WatchCompiler
559562
if (configFileName) updateExtendedConfigFilesWatches(toPath(configFileName), compilerOptions, watchOptions, WatchType.ExtendedConfigFile);
560563

561564
return configFileName ?
562-
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, close } :
563-
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, updateRootFileNames, close };
565+
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, close, getResolutionCache } :
566+
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, updateRootFileNames, close, getResolutionCache };
564567

565568
function close() {
566569
clearInvalidateResolutionsOfFailedLookupLocations();
@@ -604,6 +607,10 @@ export function createWatchProgram<T extends BuilderProgram>(host: WatchCompiler
604607
}
605608
}
606609

610+
function getResolutionCache() {
611+
return resolutionCache;
612+
}
613+
607614
function getCurrentBuilderProgram() {
608615
return builderProgram;
609616
}

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
@@ -1002,6 +1002,7 @@ export class ProjectService {
10021002
private currentPluginEnablementPromise?: Promise<void>;
10031003

10041004
/** @internal */ verifyDocumentRegistry = noop;
1005+
/** @internal */ verifyProgram: (project: Project) => void = noop;
10051006

10061007
readonly jsDocParsingMode: JSDocParsingMode | undefined;
10071008

src/server/project.ts

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

15501549
oldProgram.forEachResolvedProjectReference(resolvedProjectReference => {
15511550
if (!this.program!.getResolvedProjectReferenceByPath(resolvedProjectReference.sourceFile.path)) {
1552-
this.detachScriptInfoFromProject(resolvedProjectReference.sourceFile.fileName);
1551+
this.detachScriptInfoFromProject(resolvedProjectReference.sourceFile.fileName, /*noRemoveResolution*/ undefined, /*syncDirWatcherRemove*/ true);
15531552
}
15541553
});
15551554
}
@@ -1605,6 +1604,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
16051604
}
16061605
}
16071606

1607+
this.projectService.verifyProgram(this);
16081608
if (this.exportMapCache && !this.exportMapCache.isEmpty()) {
16091609
this.exportMapCache.releaseSymbols();
16101610
if (this.hasAddedorRemovedFiles || oldProgram && !this.program!.structureIsReused) {
@@ -1673,12 +1673,12 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
16731673
this.projectService.sendPerformanceEvent(kind, durationMs);
16741674
}
16751675

1676-
private detachScriptInfoFromProject(uncheckedFileName: string, noRemoveResolution?: boolean) {
1676+
private detachScriptInfoFromProject(uncheckedFileName: string, noRemoveResolution?: boolean, syncDirWatcherRemove?: boolean) {
16771677
const scriptInfoToDetach = this.projectService.getScriptInfo(uncheckedFileName);
16781678
if (scriptInfoToDetach) {
16791679
scriptInfoToDetach.detachFromProject(this);
16801680
if (!noRemoveResolution) {
1681-
this.resolutionCache.removeResolutionsOfFile(scriptInfoToDetach.path);
1681+
this.resolutionCache.removeResolutionsOfFile(scriptInfoToDetach.path, syncDirWatcherRemove);
16821682
}
16831683
}
16841684
}
@@ -2518,16 +2518,17 @@ export class AutoImportProviderProject extends Project {
25182518
);
25192519
if (entrypoints) {
25202520
const real = host.realpath?.(packageJson.packageDirectory);
2521-
const isSymlink = real && real !== packageJson.packageDirectory;
2521+
const realPath = real ? hostProject.toPath(real) : undefined;
2522+
const isSymlink = realPath && realPath !== hostProject.toPath(packageJson.packageDirectory);
25222523
if (isSymlink) {
25232524
symlinkCache.setSymlinkedDirectory(packageJson.packageDirectory, {
2524-
real,
2525-
realPath: hostProject.toPath(real),
2525+
real: real!,
2526+
realPath,
25262527
});
25272528
}
25282529

25292530
return mapDefined(entrypoints, entrypoint => {
2530-
const resolvedFileName = isSymlink ? entrypoint.replace(packageJson.packageDirectory, real) : entrypoint;
2531+
const resolvedFileName = isSymlink ? entrypoint.replace(packageJson.packageDirectory, real!) : entrypoint;
25312532
if (!program.getSourceFile(resolvedFileName) && !(isSymlink && program.getSourceFile(entrypoint))) {
25322533
return resolvedFileName;
25332534
}

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)