Skip to content

Commit bb0fc0d

Browse files
committed
Convert builder state to mutable data, so that later we can create builder Program out of this
1 parent 2586bb3 commit bb0fc0d

3 files changed

Lines changed: 243 additions & 28 deletions

File tree

src/compiler/builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace ts {
1313
/**
1414
* State corresponding to all the file references and shapes of the module etc
1515
*/
16-
const state = createBuilderState({
16+
const state = createBuilderStateOld({
1717
useCaseSensitiveFileNames: host.useCaseSensitiveFileNames(),
1818
createHash: host.createHash,
1919
onUpdateProgramInitialized,

src/compiler/builderState.ts

Lines changed: 237 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ namespace ts {
136136
onSourceFileRemoved(path: Path): void;
137137
}
138138

139-
export interface BuilderState {
139+
export interface BuilderStateOld {
140140
/**
141141
* Updates the program in the builder to represent new state
142142
*/
@@ -156,7 +156,65 @@ namespace ts {
156156
getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): ReadonlyArray<string>;
157157
}
158158

159-
export function createBuilderState(host: BuilderStateHost): BuilderState {
159+
export interface BuilderState {
160+
/**
161+
* Information of the file eg. its version, signature etc
162+
*/
163+
fileInfos: Map<FileInfo>;
164+
/**
165+
* Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled
166+
* Otherwise undefined
167+
* Thus non undefined value indicates, module emit
168+
*/
169+
readonly referencedMap: ReadonlyMap<ReferencedSet> | undefined;
170+
/**
171+
* Map of files that have already called update signature.
172+
* That means hence forth these files are assumed to have
173+
* no change in their signature for this version of the program
174+
*/
175+
hasCalledUpdateShapeSignature: Map<true>;
176+
/**
177+
* Cache of all files excluding default library file for the current program
178+
*/
179+
allFilesExcludingDefaultLibraryFile: ReadonlyArray<SourceFile> | undefined;
180+
/**
181+
* Cache of all the file names
182+
*/
183+
allFileNames: ReadonlyArray<string> | undefined;
184+
}
185+
186+
export function createBuilderState(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: BuilderState): BuilderState {
187+
const fileInfos = createMap<FileInfo>();
188+
const referencedMap = newProgram.getCompilerOptions().module !== ModuleKind.None ? createMap<ReferencedSet>() : undefined;
189+
const hasCalledUpdateShapeSignature = createMap<true>();
190+
const useOldState = oldState && !!oldState.referencedMap !== !!referencedMap;
191+
192+
// Create the reference map, and set the file infos
193+
for (const sourceFile of newProgram.getSourceFiles()) {
194+
const version = sourceFile.version;
195+
const oldInfo = useOldState && oldState.fileInfos.get(sourceFile.path);
196+
if (referencedMap) {
197+
const newReferences = getReferencedFiles(newProgram, sourceFile, getCanonicalFileName);
198+
if (newReferences) {
199+
referencedMap.set(sourceFile.path, newReferences);
200+
}
201+
}
202+
fileInfos.set(sourceFile.path, { version, signature: oldInfo && oldInfo.signature });
203+
}
204+
205+
oldState = undefined;
206+
newProgram = undefined;
207+
208+
return {
209+
fileInfos,
210+
referencedMap,
211+
hasCalledUpdateShapeSignature,
212+
allFilesExcludingDefaultLibraryFile: undefined,
213+
allFileNames: undefined
214+
};
215+
}
216+
217+
export function createBuilderStateOld(host: BuilderStateHost): BuilderStateOld {
160218
/**
161219
* Create the canonical file name for identity
162220
*/
@@ -171,11 +229,6 @@ namespace ts {
171229
*/
172230
const fileInfos = createMap<FileInfo>();
173231

174-
/**
175-
* true if module emit is enabled
176-
*/
177-
let isModuleEmit: boolean;
178-
179232
/**
180233
* Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled
181234
* Otherwise undefined
@@ -218,7 +271,7 @@ namespace ts {
218271
function updateProgram(newProgram: Program) {
219272
const newProgramHasModuleEmit = newProgram.getCompilerOptions().module !== ModuleKind.None;
220273
const oldReferencedMap = referencedMap;
221-
const isModuleEmitChanged = isModuleEmit !== newProgramHasModuleEmit;
274+
const isModuleEmitChanged = !!referencedMap !== newProgramHasModuleEmit;
222275
if (isModuleEmitChanged) {
223276
// Changes in the module emit, clear out everything and initialize as if first time
224277

@@ -229,8 +282,7 @@ namespace ts {
229282
referencedMap = newProgramHasModuleEmit ? createMap<ReferencedSet>() : undefined;
230283

231284
// Update the module emit
232-
isModuleEmit = newProgramHasModuleEmit;
233-
getEmitDependentFilesAffectedBy = isModuleEmit ?
285+
getEmitDependentFilesAffectedBy = newProgramHasModuleEmit ?
234286
getFilesAffectedByUpdatedShapeWhenModuleEmit :
235287
getFilesAffectedByUpdatedShapeWhenNonModuleEmit;
236288
}
@@ -326,12 +378,11 @@ namespace ts {
326378
}
327379

328380
// If this is non module emit, or its a global file, it depends on all the source files
329-
if (!isModuleEmit || (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile))) {
381+
if (!referencedMap || (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile))) {
330382
return getAllFileNames(programOfThisState);
331383
}
332384

333385
// Get the references, traversing deep from the referenceMap
334-
Debug.assert(!!referencedMap);
335386
const seenMap = createMap<true>();
336387
const queue = [sourceFile.path];
337388
while (queue.length) {
@@ -497,3 +548,177 @@ namespace ts {
497548
}
498549
}
499550
}
551+
552+
/*@internal*/
553+
namespace ts.BuilderState {
554+
type ComputeHash = (data: string) => string;
555+
556+
/**
557+
* Gets the files affected by the path from the program
558+
*/
559+
export function getFilesAffectedBy(state: BuilderState, programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, computeHash?: ComputeHash, cacheToUpdateSignature?: Map<string>): ReadonlyArray<SourceFile> {
560+
// Since the operation could be cancelled, the signatures are always stored in the cache
561+
// They will be commited once it is safe to use them
562+
// eg when calling this api from tsserver, if there is no cancellation of the operation
563+
// In the other cases the affected files signatures are commited only after the iteration through the result is complete
564+
const signatureCache = cacheToUpdateSignature || createMap();
565+
const sourceFile = programOfThisState.getSourceFileByPath(path);
566+
if (!sourceFile) {
567+
return emptyArray;
568+
}
569+
570+
if (!updateShapeSignature(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash)) {
571+
return [sourceFile];
572+
}
573+
574+
const result = (state.referencedMap ? getFilesAffectedByUpdatedShapeWhenModuleEmit : getFilesAffectedByUpdatedShapeWhenNonModuleEmit)(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash);
575+
if (!cacheToUpdateSignature) {
576+
// Commit all the signatures in the signature cache
577+
updateSignaturesFromCache(state, signatureCache);
578+
}
579+
return result;
580+
}
581+
582+
/**
583+
* Updates the signatures from the cache into state's fileinfo signatures
584+
* This should be called whenever it is safe to commit the state of the builder
585+
*/
586+
export function updateSignaturesFromCache(state: BuilderState, signatureCache: Map<string>) {
587+
signatureCache.forEach((signature, path) => {
588+
state.fileInfos.get(path).signature = signature;
589+
state.hasCalledUpdateShapeSignature.set(path, true);
590+
});
591+
}
592+
593+
/**
594+
* Returns if the shape of the signature has changed since last emit
595+
*/
596+
function updateShapeSignature(state: Readonly<BuilderState>, programOfThisState: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash | undefined) {
597+
Debug.assert(!!sourceFile);
598+
599+
// If we have cached the result for this file, that means hence forth we should assume file shape is uptodate
600+
if (state.hasCalledUpdateShapeSignature.has(sourceFile.path) || cacheToUpdateSignature.has(sourceFile.path)) {
601+
return false;
602+
}
603+
604+
const info = state.fileInfos.get(sourceFile.path);
605+
Debug.assert(!!info);
606+
607+
const prevSignature = info.signature;
608+
let latestSignature: string;
609+
if (sourceFile.isDeclarationFile) {
610+
latestSignature = sourceFile.version;
611+
}
612+
else {
613+
const emitOutput = getFileEmitOutput(programOfThisState, sourceFile, /*emitOnlyDtsFiles*/ true, cancellationToken);
614+
if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
615+
latestSignature = (computeHash || identity)(emitOutput.outputFiles[0].text);
616+
}
617+
else {
618+
latestSignature = prevSignature;
619+
}
620+
}
621+
cacheToUpdateSignature.set(sourceFile.path, latestSignature);
622+
623+
return !prevSignature || latestSignature !== prevSignature;
624+
}
625+
626+
/**
627+
* Gets the files referenced by the the file path
628+
*/
629+
function getReferencedByPaths(state: Readonly<BuilderState>, referencedFilePath: Path) {
630+
return mapDefinedIter(state.referencedMap.entries(), ([filePath, referencesInFile]) =>
631+
referencesInFile.has(referencedFilePath) ? filePath as Path : undefined
632+
);
633+
}
634+
635+
/**
636+
* For script files that contains only ambient external modules, although they are not actually external module files,
637+
* they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore,
638+
* there are no point to rebuild all script files if these special files have changed. However, if any statement
639+
* in the file is not ambient external module, we treat it as a regular script file.
640+
*/
641+
function containsOnlyAmbientModules(sourceFile: SourceFile) {
642+
for (const statement of sourceFile.statements) {
643+
if (!isModuleWithStringLiteralName(statement)) {
644+
return false;
645+
}
646+
}
647+
return true;
648+
}
649+
650+
/**
651+
* Gets all files of the program excluding the default library file
652+
*/
653+
function getAllFilesExcludingDefaultLibraryFile(state: BuilderState, programOfThisState: Program, firstSourceFile: SourceFile): ReadonlyArray<SourceFile> {
654+
// Use cached result
655+
if (state.allFilesExcludingDefaultLibraryFile) {
656+
return state.allFilesExcludingDefaultLibraryFile;
657+
}
658+
659+
let result: SourceFile[];
660+
addSourceFile(firstSourceFile);
661+
for (const sourceFile of programOfThisState.getSourceFiles()) {
662+
if (sourceFile !== firstSourceFile) {
663+
addSourceFile(sourceFile);
664+
}
665+
}
666+
state.allFilesExcludingDefaultLibraryFile = result || emptyArray;
667+
return state.allFilesExcludingDefaultLibraryFile;
668+
669+
function addSourceFile(sourceFile: SourceFile) {
670+
if (!programOfThisState.isSourceFileDefaultLibrary(sourceFile)) {
671+
(result || (result = [])).push(sourceFile);
672+
}
673+
}
674+
}
675+
676+
/**
677+
* When program emits non modular code, gets the files affected by the sourceFile whose shape has changed
678+
*/
679+
function getFilesAffectedByUpdatedShapeWhenNonModuleEmit(state: BuilderState, programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile) {
680+
const compilerOptions = programOfThisState.getCompilerOptions();
681+
// If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project,
682+
// so returning the file itself is good enough.
683+
if (compilerOptions && (compilerOptions.out || compilerOptions.outFile)) {
684+
return [sourceFileWithUpdatedShape];
685+
}
686+
return getAllFilesExcludingDefaultLibraryFile(state, programOfThisState, sourceFileWithUpdatedShape);
687+
}
688+
689+
/**
690+
* When program emits modular code, gets the files affected by the sourceFile whose shape has changed
691+
*/
692+
function getFilesAffectedByUpdatedShapeWhenModuleEmit(state: BuilderState, programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash | undefined) {
693+
if (!isExternalModule(sourceFileWithUpdatedShape) && !containsOnlyAmbientModules(sourceFileWithUpdatedShape)) {
694+
return getAllFilesExcludingDefaultLibraryFile(state, programOfThisState, sourceFileWithUpdatedShape);
695+
}
696+
697+
const compilerOptions = programOfThisState.getCompilerOptions();
698+
if (compilerOptions && (compilerOptions.isolatedModules || compilerOptions.out || compilerOptions.outFile)) {
699+
return [sourceFileWithUpdatedShape];
700+
}
701+
702+
// Now we need to if each file in the referencedBy list has a shape change as well.
703+
// Because if so, its own referencedBy files need to be saved as well to make the
704+
// emitting result consistent with files on disk.
705+
const seenFileNamesMap = createMap<SourceFile>();
706+
707+
// Start with the paths this file was referenced by
708+
seenFileNamesMap.set(sourceFileWithUpdatedShape.path, sourceFileWithUpdatedShape);
709+
const queue = getReferencedByPaths(state, sourceFileWithUpdatedShape.path);
710+
while (queue.length > 0) {
711+
const currentPath = queue.pop();
712+
if (!seenFileNamesMap.has(currentPath)) {
713+
const currentSourceFile = programOfThisState.getSourceFileByPath(currentPath);
714+
seenFileNamesMap.set(currentPath, currentSourceFile);
715+
if (currentSourceFile && updateShapeSignature(state, programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken, computeHash)) {
716+
queue.push(...getReferencedByPaths(state, currentPath));
717+
}
718+
}
719+
}
720+
721+
// Return array of values that needs emit
722+
return flatMapIter(seenFileNamesMap.values(), value => value);
723+
}
724+
}

src/server/project.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ namespace ts.server {
139139
/*@internal*/
140140
resolutionCache: ResolutionCache;
141141

142-
private builder: BuilderState | undefined;
142+
private builderState: BuilderState | undefined;
143143
/**
144144
* Set of files names that were updated since the last call to getChangesSinceVersion.
145145
*/
@@ -460,18 +460,8 @@ namespace ts.server {
460460
return [];
461461
}
462462
this.updateGraph();
463-
if (!this.builder) {
464-
this.builder = createBuilderState({
465-
useCaseSensitiveFileNames: this.useCaseSensitiveFileNames(),
466-
createHash: data => this.projectService.host.createHash(data),
467-
onUpdateProgramInitialized: noop,
468-
onSourceFileAdd: noop,
469-
onSourceFileChanged: noop,
470-
onSourceFileRemoved: noop
471-
});
472-
}
473-
this.builder.updateProgram(this.program);
474-
return mapDefined(this.builder.getFilesAffectedBy(this.program, scriptInfo.path, this.cancellationToken),
463+
this.builderState = createBuilderState(this.program, this.projectService.toCanonicalFileName, this.builderState);
464+
return mapDefined(BuilderState.getFilesAffectedBy(this.builderState, this.program, scriptInfo.path, this.cancellationToken, data => this.projectService.host.createHash(data)),
475465
sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)) ? sourceFile.fileName : undefined);
476466
}
477467

@@ -507,7 +497,7 @@ namespace ts.server {
507497
}
508498
this.languageService.cleanupSemanticCache();
509499
this.languageServiceEnabled = false;
510-
this.builder = undefined;
500+
this.builderState = undefined;
511501
this.resolutionCache.closeTypeRootsWatch();
512502
this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false);
513503
}
@@ -548,7 +538,7 @@ namespace ts.server {
548538
this.rootFilesMap = undefined;
549539
this.externalFiles = undefined;
550540
this.program = undefined;
551-
this.builder = undefined;
541+
this.builderState = undefined;
552542
this.resolutionCache.clear();
553543
this.resolutionCache = undefined;
554544
this.cachedUnresolvedImportsPerFile = undefined;

0 commit comments

Comments
 (0)