mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-19 20:37:00 -05:00
Use builder state in the semantic/emit builder as well
This commit is contained in:
@@ -7,52 +7,118 @@ namespace ts {
|
||||
BuilderKindEmitAndSemanticDiagnostics
|
||||
}
|
||||
|
||||
export function createBuilder(host: BuilderHost, builderKind: BuilderKind.BuilderKindSemanticDiagnostics): SemanticDiagnosticsBuilder;
|
||||
export function createBuilder(host: BuilderHost, builderKind: BuilderKind.BuilderKindEmitAndSemanticDiagnostics): EmitAndSemanticDiagnosticsBuilder;
|
||||
export function createBuilder(host: BuilderHost, builderKind: BuilderKind) {
|
||||
/**
|
||||
* State corresponding to all the file references and shapes of the module etc
|
||||
*/
|
||||
const state = createBuilderStateOld({
|
||||
useCaseSensitiveFileNames: host.useCaseSensitiveFileNames(),
|
||||
createHash: host.createHash,
|
||||
onUpdateProgramInitialized,
|
||||
onSourceFileAdd: addToChangedFilesSet,
|
||||
onSourceFileChanged: path => { addToChangedFilesSet(path); deleteSemanticDiagnostics(path); },
|
||||
onSourceFileRemoved: deleteSemanticDiagnostics
|
||||
});
|
||||
|
||||
interface BuilderStateWithChangedFiles extends BuilderState {
|
||||
/**
|
||||
* Cache of semantic diagnostics for files with their Path being the key
|
||||
*/
|
||||
const semanticDiagnosticsPerFile = createMap<ReadonlyArray<Diagnostic>>();
|
||||
|
||||
semanticDiagnosticsPerFile: Map<ReadonlyArray<Diagnostic>> | undefined;
|
||||
/**
|
||||
* The map has key by source file's path that has been changed
|
||||
*/
|
||||
const changedFilesSet = createMap<true>();
|
||||
|
||||
changedFilesSet: Map<true>;
|
||||
/**
|
||||
* Set of affected files being iterated
|
||||
*/
|
||||
let affectedFiles: ReadonlyArray<SourceFile> | undefined;
|
||||
affectedFiles: ReadonlyArray<SourceFile> | undefined;
|
||||
/**
|
||||
* Current index to retrieve affected file from
|
||||
*/
|
||||
let affectedFilesIndex = 0;
|
||||
affectedFilesIndex: number | undefined;
|
||||
/**
|
||||
* Current changed file for iterating over affected files
|
||||
*/
|
||||
let currentChangedFilePath: Path | undefined;
|
||||
currentChangedFilePath: Path | undefined;
|
||||
/**
|
||||
* Map of file signatures, with key being file path, calculated while getting current changed file's affected files
|
||||
* These will be commited whenever the iteration through affected files of current changed file is complete
|
||||
*/
|
||||
const currentAffectedFilesSignatures = createMap<string>();
|
||||
currentAffectedFilesSignatures: Map<string> | undefined;
|
||||
/**
|
||||
* Already seen affected files
|
||||
*/
|
||||
const seenAffectedFiles = createMap<true>();
|
||||
seenAffectedFiles: Map<true> | undefined;
|
||||
}
|
||||
|
||||
function hasSameKeys<T, U>(map1: ReadonlyMap<T> | undefined, map2: ReadonlyMap<U> | undefined) {
|
||||
if (map1 === undefined) {
|
||||
return map2 === undefined;
|
||||
}
|
||||
if (map2 === undefined) {
|
||||
return map1 === undefined;
|
||||
}
|
||||
// Has same size and every key is present in both maps
|
||||
return map1.size === map2.size && !forEachKey(map1, key => !map2.has(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the state so that we can iterate on changedFiles/affected files
|
||||
*/
|
||||
function createBuilderStateWithChangedFiles(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: Readonly<BuilderStateWithChangedFiles>): BuilderStateWithChangedFiles {
|
||||
const state = BuilderState.create(newProgram, getCanonicalFileName, oldState) as BuilderStateWithChangedFiles;
|
||||
const compilerOptions = newProgram.getCompilerOptions();
|
||||
if (!compilerOptions.outFile && !compilerOptions.out) {
|
||||
state.semanticDiagnosticsPerFile = createMap<ReadonlyArray<Diagnostic>>();
|
||||
}
|
||||
state.changedFilesSet = createMap<true>();
|
||||
const useOldState = BuilderState.canReuseOldState(state.referencedMap, oldState);
|
||||
const canCopySemanticDiagnostics = useOldState && oldState.semanticDiagnosticsPerFile && !!state.semanticDiagnosticsPerFile;
|
||||
if (useOldState) {
|
||||
// Verify the sanity of old state
|
||||
if (!oldState.currentChangedFilePath) {
|
||||
Debug.assert(!oldState.affectedFiles && (!oldState.currentAffectedFilesSignatures || !oldState.currentAffectedFilesSignatures.size), "Cannot reuse if only few affected files of currentChangedFile were iterated");
|
||||
}
|
||||
if (canCopySemanticDiagnostics) {
|
||||
Debug.assert(!forEachKey(oldState.changedFilesSet, path => oldState.semanticDiagnosticsPerFile.has(path)), "Semantic diagnostics shouldnt be available for changed files");
|
||||
}
|
||||
|
||||
// Copy old state's changed files set
|
||||
copyEntries(oldState.changedFilesSet, state.changedFilesSet);
|
||||
}
|
||||
|
||||
// Update changed files and copy semantic diagnostics if we can
|
||||
const referencedMap = state.referencedMap;
|
||||
const oldReferencedMap = useOldState && oldState.referencedMap;
|
||||
state.fileInfos.forEach((info, sourceFilePath) => {
|
||||
let oldInfo: Readonly<BuilderState.FileInfo>;
|
||||
let newReferences: BuilderState.ReferencedSet;
|
||||
|
||||
// if not using old state, every file is changed
|
||||
if (!useOldState ||
|
||||
// File wasnt present in old state
|
||||
!(oldInfo = oldState.fileInfos.get(sourceFilePath)) ||
|
||||
// versions dont match
|
||||
oldInfo.version !== info.version ||
|
||||
// Referenced files changed
|
||||
!hasSameKeys(newReferences = referencedMap && referencedMap.get(sourceFilePath), oldReferencedMap && oldReferencedMap.get(sourceFilePath)) ||
|
||||
// Referenced file was deleted in the new program
|
||||
newReferences && forEachKey(newReferences, path => !state.fileInfos.has(path) && oldState.fileInfos.has(path))) {
|
||||
// Register file as changed file and do not copy semantic diagnostics, since all changed files need to be re-evaluated
|
||||
state.changedFilesSet.set(sourceFilePath, true);
|
||||
}
|
||||
else if (canCopySemanticDiagnostics) {
|
||||
// Unchanged file copy diagnostics
|
||||
const diagnostics = oldState.semanticDiagnosticsPerFile.get(sourceFilePath);
|
||||
if (diagnostics) {
|
||||
state.semanticDiagnosticsPerFile.set(sourceFilePath, diagnostics);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export function createBuilder(host: BuilderHost, builderKind: BuilderKind.BuilderKindSemanticDiagnostics): SemanticDiagnosticsBuilder;
|
||||
export function createBuilder(host: BuilderHost, builderKind: BuilderKind.BuilderKindEmitAndSemanticDiagnostics): EmitAndSemanticDiagnosticsBuilder;
|
||||
export function createBuilder(host: BuilderHost, builderKind: BuilderKind) {
|
||||
/**
|
||||
* Create the canonical file name for identity
|
||||
*/
|
||||
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames());
|
||||
/**
|
||||
* Computing hash to for signature verification
|
||||
*/
|
||||
const computeHash = host.createHash || identity;
|
||||
let state: BuilderStateWithChangedFiles;
|
||||
|
||||
switch (builderKind) {
|
||||
case BuilderKind.BuilderKindSemanticDiagnostics:
|
||||
@@ -81,55 +147,12 @@ namespace ts {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize changedFiles, affected files set, cached diagnostics, signatures
|
||||
*/
|
||||
function onUpdateProgramInitialized(isModuleEmitChanged: boolean) {
|
||||
if (isModuleEmitChanged) {
|
||||
// Changes in the module emit, clear out everything and initialize as if first time
|
||||
|
||||
// Clear file information and semantic diagnostics
|
||||
semanticDiagnosticsPerFile.clear();
|
||||
|
||||
// Clear changed files and affected files information
|
||||
changedFilesSet.clear();
|
||||
affectedFiles = undefined;
|
||||
currentChangedFilePath = undefined;
|
||||
currentAffectedFilesSignatures.clear();
|
||||
}
|
||||
else {
|
||||
if (currentChangedFilePath) {
|
||||
// Remove the diagnostics for all the affected files since we should resume the state such that
|
||||
// the whole iteration on currentChangedFile never happened
|
||||
affectedFiles.forEach(sourceFile => deleteSemanticDiagnostics(sourceFile.path));
|
||||
affectedFiles = undefined;
|
||||
currentAffectedFilesSignatures.clear();
|
||||
}
|
||||
else {
|
||||
// Verify the sanity of old state
|
||||
Debug.assert(!affectedFiles && !currentAffectedFilesSignatures.size, "Cannot reuse if only few affected files of currentChangedFile were iterated");
|
||||
}
|
||||
Debug.assert(!forEachKey(changedFilesSet, path => semanticDiagnosticsPerFile.has(path)), "Semantic diagnostics shouldnt be available for changed files");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add file to the changed files set
|
||||
*/
|
||||
function addToChangedFilesSet(path: Path) {
|
||||
changedFilesSet.set(path, true);
|
||||
}
|
||||
|
||||
function deleteSemanticDiagnostics(path: Path) {
|
||||
semanticDiagnosticsPerFile.delete(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current state to reflect new program
|
||||
* Updates changed files, references, file infos etc which happens through the state callbacks
|
||||
*/
|
||||
function updateProgram(newProgram: Program) {
|
||||
state.updateProgram(newProgram);
|
||||
state = createBuilderStateWithChangedFiles(newProgram, getCanonicalFileName, state);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,11 +163,15 @@ namespace ts {
|
||||
*/
|
||||
function getNextAffectedFile(programOfThisState: Program, cancellationToken: CancellationToken | undefined): SourceFile | Program | undefined {
|
||||
while (true) {
|
||||
const { affectedFiles } = state;
|
||||
if (affectedFiles) {
|
||||
const { seenAffectedFiles, semanticDiagnosticsPerFile } = state;
|
||||
let { affectedFilesIndex } = state;
|
||||
while (affectedFilesIndex < affectedFiles.length) {
|
||||
const affectedFile = affectedFiles[affectedFilesIndex];
|
||||
if (!seenAffectedFiles.has(affectedFile.path)) {
|
||||
// Set the next affected file as seen and remove the cached semantic diagnostics
|
||||
state.affectedFilesIndex = affectedFilesIndex;
|
||||
semanticDiagnosticsPerFile.delete(affectedFile.path);
|
||||
return affectedFile;
|
||||
}
|
||||
@@ -153,16 +180,16 @@ namespace ts {
|
||||
}
|
||||
|
||||
// Remove the changed file from the change set
|
||||
changedFilesSet.delete(currentChangedFilePath);
|
||||
currentChangedFilePath = undefined;
|
||||
state.changedFilesSet.delete(state.currentChangedFilePath);
|
||||
state.currentChangedFilePath = undefined;
|
||||
// Commit the changes in file signature
|
||||
state.updateSignaturesFromCache(currentAffectedFilesSignatures);
|
||||
currentAffectedFilesSignatures.clear();
|
||||
affectedFiles = undefined;
|
||||
BuilderState.updateSignaturesFromCache(state, state.currentAffectedFilesSignatures);
|
||||
state.currentAffectedFilesSignatures.clear();
|
||||
state.affectedFiles = undefined;
|
||||
}
|
||||
|
||||
// Get next changed file
|
||||
const nextKey = changedFilesSet.keys().next();
|
||||
const nextKey = state.changedFilesSet.keys().next();
|
||||
if (nextKey.done) {
|
||||
// Done
|
||||
return undefined;
|
||||
@@ -172,16 +199,17 @@ namespace ts {
|
||||
// With --out or --outFile all outputs go into single file
|
||||
// so operations are performed directly on program, return program
|
||||
if (compilerOptions.outFile || compilerOptions.out) {
|
||||
Debug.assert(semanticDiagnosticsPerFile.size === 0);
|
||||
Debug.assert(!state.semanticDiagnosticsPerFile);
|
||||
return programOfThisState;
|
||||
}
|
||||
|
||||
// Get next batch of affected files
|
||||
currentAffectedFilesSignatures.clear();
|
||||
affectedFiles = state.getFilesAffectedBy(programOfThisState, nextKey.value as Path, cancellationToken, currentAffectedFilesSignatures);
|
||||
currentChangedFilePath = nextKey.value as Path;
|
||||
semanticDiagnosticsPerFile.delete(currentChangedFilePath);
|
||||
affectedFilesIndex = 0;
|
||||
state.currentAffectedFilesSignatures = state.currentAffectedFilesSignatures || createMap();
|
||||
state.affectedFiles = BuilderState.getFilesAffectedBy(state, programOfThisState, nextKey.value as Path, cancellationToken, computeHash, state.currentAffectedFilesSignatures);
|
||||
state.currentChangedFilePath = nextKey.value as Path;
|
||||
state.semanticDiagnosticsPerFile.delete(nextKey.value as Path);
|
||||
state.affectedFilesIndex = 0;
|
||||
state.seenAffectedFiles = state.seenAffectedFiles || createMap<true>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,11 +219,11 @@ namespace ts {
|
||||
*/
|
||||
function doneWithAffectedFile(programOfThisState: Program, affected: SourceFile | Program) {
|
||||
if (affected === programOfThisState) {
|
||||
changedFilesSet.clear();
|
||||
state.changedFilesSet.clear();
|
||||
}
|
||||
else {
|
||||
seenAffectedFiles.set((<SourceFile>affected).path, true);
|
||||
affectedFilesIndex++;
|
||||
state.seenAffectedFiles.set((affected as SourceFile).path, true);
|
||||
state.affectedFilesIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,10 +305,10 @@ namespace ts {
|
||||
* Note that it is assumed that the when asked about semantic diagnostics, the file has been taken out of affected files
|
||||
*/
|
||||
function getSemanticDiagnostics(programOfThisState: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray<Diagnostic> {
|
||||
Debug.assert(!affectedFiles || affectedFiles[affectedFilesIndex - 1] !== sourceFile || !semanticDiagnosticsPerFile.has(sourceFile.path));
|
||||
Debug.assert(!state.affectedFiles || state.affectedFiles[state.affectedFilesIndex - 1] !== sourceFile || !state.semanticDiagnosticsPerFile.has(sourceFile.path));
|
||||
const compilerOptions = programOfThisState.getCompilerOptions();
|
||||
if (compilerOptions.outFile || compilerOptions.out) {
|
||||
Debug.assert(semanticDiagnosticsPerFile.size === 0);
|
||||
Debug.assert(!state.semanticDiagnosticsPerFile);
|
||||
// We dont need to cache the diagnostics just return them from program
|
||||
return programOfThisState.getSemanticDiagnostics(sourceFile, cancellationToken);
|
||||
}
|
||||
@@ -302,7 +330,7 @@ namespace ts {
|
||||
*/
|
||||
function getSemanticDiagnosticsOfFile(program: Program, sourceFile: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray<Diagnostic> {
|
||||
const path = sourceFile.path;
|
||||
const cachedDiagnostics = semanticDiagnosticsPerFile.get(path);
|
||||
const cachedDiagnostics = state.semanticDiagnosticsPerFile.get(path);
|
||||
// Report the semantic diagnostics from the cache if we already have those diagnostics present
|
||||
if (cachedDiagnostics) {
|
||||
return cachedDiagnostics;
|
||||
@@ -310,7 +338,7 @@ namespace ts {
|
||||
|
||||
// Diagnostics werent cached, get them from program, and cache the result
|
||||
const diagnostics = program.getSemanticDiagnostics(sourceFile, cancellationToken);
|
||||
semanticDiagnosticsPerFile.set(path, diagnostics);
|
||||
state.semanticDiagnosticsPerFile.set(path, diagnostics);
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
@@ -318,7 +346,7 @@ namespace ts {
|
||||
* Get all the dependencies of the sourceFile
|
||||
*/
|
||||
function getAllDependencies(programOfThisState: Program, sourceFile: SourceFile) {
|
||||
return state.getAllDependencies(programOfThisState, sourceFile);
|
||||
return BuilderState.getAllDependencies(state, programOfThisState, sourceFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,44 +25,51 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
export interface BuilderState {
|
||||
/**
|
||||
* Information of the file eg. its version, signature etc
|
||||
*/
|
||||
fileInfos: Map<BuilderState.FileInfo>;
|
||||
/**
|
||||
* Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled
|
||||
* Otherwise undefined
|
||||
* Thus non undefined value indicates, module emit
|
||||
*/
|
||||
readonly referencedMap: ReadonlyMap<BuilderState.ReferencedSet> | undefined;
|
||||
/**
|
||||
* Map of files that have already called update signature.
|
||||
* That means hence forth these files are assumed to have
|
||||
* no change in their signature for this version of the program
|
||||
*/
|
||||
hasCalledUpdateShapeSignature: Map<true>;
|
||||
/**
|
||||
* Cache of all files excluding default library file for the current program
|
||||
*/
|
||||
allFilesExcludingDefaultLibraryFile: ReadonlyArray<SourceFile> | undefined;
|
||||
/**
|
||||
* Cache of all the file names
|
||||
*/
|
||||
allFileNames: ReadonlyArray<string> | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
namespace ts.BuilderState {
|
||||
/**
|
||||
* Information about the source file: Its version and optional signature from last emit
|
||||
*/
|
||||
interface FileInfo {
|
||||
version: string;
|
||||
signature?: string;
|
||||
export interface FileInfo {
|
||||
readonly version: string;
|
||||
signature: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Referenced files with values for the keys as referenced file's path to be true
|
||||
*/
|
||||
type ReferencedSet = ReadonlyMap<true>;
|
||||
|
||||
function hasSameKeys<T, U>(map1: ReadonlyMap<T> | undefined, map2: ReadonlyMap<U> | undefined) {
|
||||
if (map1 === undefined) {
|
||||
return map2 === undefined;
|
||||
}
|
||||
if (map2 === undefined) {
|
||||
return map1 === undefined;
|
||||
}
|
||||
// Has same size and every key is present in both maps
|
||||
return map1.size === map2.size && !forEachKey(map1, key => !map2.has(key));
|
||||
}
|
||||
|
||||
export type ReferencedSet = ReadonlyMap<true>;
|
||||
/**
|
||||
* For script files that contains only ambient external modules, although they are not actually external module files,
|
||||
* they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore,
|
||||
* there are no point to rebuild all script files if these special files have changed. However, if any statement
|
||||
* in the file is not ambient external module, we treat it as a regular script file.
|
||||
* Compute the hash to store the shape of the file
|
||||
*/
|
||||
function containsOnlyAmbientModules(sourceFile: SourceFile) {
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (!isModuleWithStringLiteralName(statement)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
export type ComputeHash = (data: string) => string;
|
||||
|
||||
/**
|
||||
* Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true
|
||||
@@ -118,76 +125,21 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
export interface BuilderStateHost {
|
||||
/**
|
||||
* true if file names are treated with case sensitivity
|
||||
*/
|
||||
useCaseSensitiveFileNames: boolean;
|
||||
/**
|
||||
* if provided this would be used this hash instead of actual file shape text for detecting changes
|
||||
*/
|
||||
createHash?: (data: string) => string;
|
||||
/**
|
||||
* Called when programState is initialized, indicating if isModuleEmit is changed
|
||||
*/
|
||||
onUpdateProgramInitialized(isModuleEmitChanged: boolean): void;
|
||||
onSourceFileAdd(path: Path): void;
|
||||
onSourceFileChanged(path: Path): void;
|
||||
onSourceFileRemoved(path: Path): void;
|
||||
/**
|
||||
* Returns true if oldState is reusable, that is the emitKind = module/non module has not changed
|
||||
*/
|
||||
export function canReuseOldState(newReferencedMap: ReadonlyMap<ReferencedSet>, oldState: Readonly<BuilderState> | undefined) {
|
||||
return oldState && !oldState.referencedMap === !newReferencedMap;
|
||||
}
|
||||
|
||||
export interface BuilderStateOld {
|
||||
/**
|
||||
* Updates the program in the builder to represent new state
|
||||
*/
|
||||
updateProgram(newProgram: Program): void;
|
||||
/**
|
||||
* Gets the files affected by the file path
|
||||
*/
|
||||
getFilesAffectedBy(programOfThisState: Program, path: Path, cancellationToken: CancellationToken, cacheToUpdateSignature?: Map<string>): ReadonlyArray<SourceFile>;
|
||||
/**
|
||||
* Updates the signatures from the cache
|
||||
* This should be called whenever it is safe to commit the state of the builder
|
||||
*/
|
||||
updateSignaturesFromCache(signatureCache: Map<string>): void;
|
||||
/**
|
||||
* Get all the dependencies of the sourceFile
|
||||
*/
|
||||
getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
export interface BuilderState {
|
||||
/**
|
||||
* Information of the file eg. its version, signature etc
|
||||
*/
|
||||
fileInfos: Map<FileInfo>;
|
||||
/**
|
||||
* Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled
|
||||
* Otherwise undefined
|
||||
* Thus non undefined value indicates, module emit
|
||||
*/
|
||||
readonly referencedMap: ReadonlyMap<ReferencedSet> | undefined;
|
||||
/**
|
||||
* Map of files that have already called update signature.
|
||||
* That means hence forth these files are assumed to have
|
||||
* no change in their signature for this version of the program
|
||||
*/
|
||||
hasCalledUpdateShapeSignature: Map<true>;
|
||||
/**
|
||||
* Cache of all files excluding default library file for the current program
|
||||
*/
|
||||
allFilesExcludingDefaultLibraryFile: ReadonlyArray<SourceFile> | undefined;
|
||||
/**
|
||||
* Cache of all the file names
|
||||
*/
|
||||
allFileNames: ReadonlyArray<string> | undefined;
|
||||
}
|
||||
|
||||
export function createBuilderState(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: BuilderState): BuilderState {
|
||||
/**
|
||||
* Creates the state of file references and signature for the new program from oldState if it is safe
|
||||
*/
|
||||
export function create(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: Readonly<BuilderState>): BuilderState {
|
||||
const fileInfos = createMap<FileInfo>();
|
||||
const referencedMap = newProgram.getCompilerOptions().module !== ModuleKind.None ? createMap<ReferencedSet>() : undefined;
|
||||
const hasCalledUpdateShapeSignature = createMap<true>();
|
||||
const useOldState = oldState && !!oldState.referencedMap !== !!referencedMap;
|
||||
const useOldState = canReuseOldState(referencedMap, oldState);
|
||||
|
||||
// Create the reference map, and set the file infos
|
||||
for (const sourceFile of newProgram.getSourceFiles()) {
|
||||
@@ -202,9 +154,6 @@ namespace ts {
|
||||
fileInfos.set(sourceFile.path, { version, signature: oldInfo && oldInfo.signature });
|
||||
}
|
||||
|
||||
oldState = undefined;
|
||||
newProgram = undefined;
|
||||
|
||||
return {
|
||||
fileInfos,
|
||||
referencedMap,
|
||||
@@ -214,349 +163,10 @@ namespace ts {
|
||||
};
|
||||
}
|
||||
|
||||
export function createBuilderStateOld(host: BuilderStateHost): BuilderStateOld {
|
||||
/**
|
||||
* Create the canonical file name for identity
|
||||
*/
|
||||
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
|
||||
/**
|
||||
* Computing hash to for signature verification
|
||||
*/
|
||||
const computeHash = host.createHash || identity;
|
||||
|
||||
/**
|
||||
* Information of the file eg. its version, signature etc
|
||||
*/
|
||||
const fileInfos = createMap<FileInfo>();
|
||||
|
||||
/**
|
||||
* Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled
|
||||
* Otherwise undefined
|
||||
*/
|
||||
let referencedMap: Map<ReferencedSet> | undefined;
|
||||
|
||||
/**
|
||||
* Get the files affected by the source file.
|
||||
* This is dependent on whether its a module emit or not and hence function expression
|
||||
*/
|
||||
let getEmitDependentFilesAffectedBy: (programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined) => ReadonlyArray<SourceFile>;
|
||||
|
||||
/**
|
||||
* Map of files that have already called update signature.
|
||||
* That means hence forth these files are assumed to have
|
||||
* no change in their signature for this version of the program
|
||||
*/
|
||||
const hasCalledUpdateShapeSignature = createMap<true>();
|
||||
|
||||
/**
|
||||
* Cache of all files excluding default library file for the current program
|
||||
*/
|
||||
let allFilesExcludingDefaultLibraryFile: ReadonlyArray<SourceFile> | undefined;
|
||||
/**
|
||||
* Cache of all the file names
|
||||
*/
|
||||
let allFileNames: ReadonlyArray<string> | undefined;
|
||||
|
||||
return {
|
||||
updateProgram,
|
||||
getFilesAffectedBy,
|
||||
getAllDependencies,
|
||||
updateSignaturesFromCache
|
||||
};
|
||||
|
||||
/**
|
||||
* Update current state to reflect new program
|
||||
* Updates changed files, references, file infos etc
|
||||
*/
|
||||
function updateProgram(newProgram: Program) {
|
||||
const newProgramHasModuleEmit = newProgram.getCompilerOptions().module !== ModuleKind.None;
|
||||
const oldReferencedMap = referencedMap;
|
||||
const isModuleEmitChanged = !!referencedMap !== newProgramHasModuleEmit;
|
||||
if (isModuleEmitChanged) {
|
||||
// Changes in the module emit, clear out everything and initialize as if first time
|
||||
|
||||
// Clear file information
|
||||
fileInfos.clear();
|
||||
|
||||
// Update the reference map creation
|
||||
referencedMap = newProgramHasModuleEmit ? createMap<ReferencedSet>() : undefined;
|
||||
|
||||
// Update the module emit
|
||||
getEmitDependentFilesAffectedBy = newProgramHasModuleEmit ?
|
||||
getFilesAffectedByUpdatedShapeWhenModuleEmit :
|
||||
getFilesAffectedByUpdatedShapeWhenNonModuleEmit;
|
||||
}
|
||||
host.onUpdateProgramInitialized(isModuleEmitChanged);
|
||||
|
||||
// Clear datas that cant be retained beyond previous state
|
||||
hasCalledUpdateShapeSignature.clear();
|
||||
allFilesExcludingDefaultLibraryFile = undefined;
|
||||
allFileNames = undefined;
|
||||
|
||||
// Create the reference map and update changed files
|
||||
for (const sourceFile of newProgram.getSourceFiles()) {
|
||||
const version = sourceFile.version;
|
||||
const newReferences = referencedMap && getReferencedFiles(newProgram, sourceFile, getCanonicalFileName);
|
||||
const oldInfo = fileInfos.get(sourceFile.path);
|
||||
let oldReferences: ReferencedSet;
|
||||
|
||||
// Register changed file if its new file or we arent reusing old state
|
||||
if (!oldInfo) {
|
||||
// New file: Set the file info
|
||||
fileInfos.set(sourceFile.path, { version });
|
||||
host.onSourceFileAdd(sourceFile.path);
|
||||
}
|
||||
// versions dont match
|
||||
else if (oldInfo.version !== version ||
|
||||
// Referenced files changed
|
||||
!hasSameKeys(newReferences, (oldReferences = oldReferencedMap && oldReferencedMap.get(sourceFile.path))) ||
|
||||
// Referenced file was deleted in the new program
|
||||
newReferences && forEachKey(newReferences, path => !newProgram.getSourceFileByPath(path as Path) && fileInfos.has(path))) {
|
||||
|
||||
// Changed file: Update the version, set as changed file
|
||||
oldInfo.version = version;
|
||||
host.onSourceFileChanged(sourceFile.path);
|
||||
}
|
||||
|
||||
// Set the references
|
||||
if (newReferences) {
|
||||
referencedMap.set(sourceFile.path, newReferences);
|
||||
}
|
||||
else if (referencedMap) {
|
||||
referencedMap.delete(sourceFile.path);
|
||||
}
|
||||
}
|
||||
|
||||
// For removed files, remove the semantic diagnostics and file info
|
||||
if (fileInfos.size > newProgram.getSourceFiles().length) {
|
||||
fileInfos.forEach((_value, path) => {
|
||||
if (!newProgram.getSourceFileByPath(path as Path)) {
|
||||
fileInfos.delete(path);
|
||||
host.onSourceFileRemoved(path as Path);
|
||||
if (referencedMap) {
|
||||
referencedMap.delete(path);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the files affected by the path from the program
|
||||
*/
|
||||
function getFilesAffectedBy(programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, cacheToUpdateSignature?: Map<string>): ReadonlyArray<SourceFile> {
|
||||
// Since the operation could be cancelled, the signatures are always stored in the cache
|
||||
// They will be commited once it is safe to use them
|
||||
// eg when calling this api from tsserver, if there is no cancellation of the operation
|
||||
// In the other cases the affected files signatures are commited only after the iteration through the result is complete
|
||||
const signatureCache = cacheToUpdateSignature || createMap();
|
||||
const sourceFile = programOfThisState.getSourceFileByPath(path);
|
||||
if (!sourceFile) {
|
||||
return emptyArray;
|
||||
}
|
||||
|
||||
if (!updateShapeSignature(programOfThisState, sourceFile, signatureCache, cancellationToken)) {
|
||||
return [sourceFile];
|
||||
}
|
||||
|
||||
const result = getEmitDependentFilesAffectedBy(programOfThisState, sourceFile, signatureCache, cancellationToken);
|
||||
if (!cacheToUpdateSignature) {
|
||||
// Commit all the signatures in the signature cache
|
||||
updateSignaturesFromCache(signatureCache);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the dependencies of the sourceFile
|
||||
*/
|
||||
function getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): ReadonlyArray<string> {
|
||||
const compilerOptions = programOfThisState.getCompilerOptions();
|
||||
// With --out or --outFile all outputs go into single file, all files depend on each other
|
||||
if (compilerOptions.outFile || compilerOptions.out) {
|
||||
return getAllFileNames(programOfThisState);
|
||||
}
|
||||
|
||||
// If this is non module emit, or its a global file, it depends on all the source files
|
||||
if (!referencedMap || (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile))) {
|
||||
return getAllFileNames(programOfThisState);
|
||||
}
|
||||
|
||||
// Get the references, traversing deep from the referenceMap
|
||||
const seenMap = createMap<true>();
|
||||
const queue = [sourceFile.path];
|
||||
while (queue.length) {
|
||||
const path = queue.pop();
|
||||
if (!seenMap.has(path)) {
|
||||
seenMap.set(path, true);
|
||||
const references = referencedMap.get(path);
|
||||
if (references) {
|
||||
const iterator = references.keys();
|
||||
for (let { value, done } = iterator.next(); !done; { value, done } = iterator.next()) {
|
||||
queue.push(value as Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flatMapIter(seenMap.keys(), path => {
|
||||
const file = programOfThisState.getSourceFileByPath(path as Path);
|
||||
if (file) {
|
||||
return file.fileName;
|
||||
}
|
||||
return path;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the names of all files from the program
|
||||
*/
|
||||
function getAllFileNames(programOfThisState: Program): ReadonlyArray<string> {
|
||||
if (!allFileNames) {
|
||||
allFileNames = programOfThisState.getSourceFiles().map(file => file.fileName);
|
||||
}
|
||||
return allFileNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the signatures from the cache
|
||||
* This should be called whenever it is safe to commit the state of the builder
|
||||
*/
|
||||
function updateSignaturesFromCache(signatureCache: Map<string>) {
|
||||
signatureCache.forEach((signature, path) => {
|
||||
fileInfos.get(path).signature = signature;
|
||||
hasCalledUpdateShapeSignature.set(path, true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the shape of the signature has changed since last emit
|
||||
* Note that it also updates the current signature as the latest signature for the file
|
||||
*/
|
||||
function updateShapeSignature(program: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined) {
|
||||
Debug.assert(!!sourceFile);
|
||||
|
||||
// If we have cached the result for this file, that means hence forth we should assume file shape is uptodate
|
||||
if (hasCalledUpdateShapeSignature.has(sourceFile.path) || cacheToUpdateSignature.has(sourceFile.path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const info = fileInfos.get(sourceFile.path);
|
||||
Debug.assert(!!info);
|
||||
|
||||
const prevSignature = info.signature;
|
||||
let latestSignature: string;
|
||||
if (sourceFile.isDeclarationFile) {
|
||||
latestSignature = sourceFile.version;
|
||||
}
|
||||
else {
|
||||
const emitOutput = getFileEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true, cancellationToken);
|
||||
if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
|
||||
latestSignature = computeHash(emitOutput.outputFiles[0].text);
|
||||
}
|
||||
else {
|
||||
latestSignature = prevSignature;
|
||||
}
|
||||
}
|
||||
cacheToUpdateSignature.set(sourceFile.path, latestSignature);
|
||||
|
||||
return !prevSignature || latestSignature !== prevSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the files referenced by the the file path
|
||||
*/
|
||||
function getReferencedByPaths(referencedFilePath: Path) {
|
||||
return mapDefinedIter(referencedMap.entries(), ([filePath, referencesInFile]) =>
|
||||
referencesInFile.has(referencedFilePath) ? filePath as Path : undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all files of the program excluding the default library file
|
||||
*/
|
||||
function getAllFilesExcludingDefaultLibraryFile(program: Program, firstSourceFile: SourceFile): ReadonlyArray<SourceFile> {
|
||||
// Use cached result
|
||||
if (allFilesExcludingDefaultLibraryFile) {
|
||||
return allFilesExcludingDefaultLibraryFile;
|
||||
}
|
||||
|
||||
let result: SourceFile[];
|
||||
addSourceFile(firstSourceFile);
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
if (sourceFile !== firstSourceFile) {
|
||||
addSourceFile(sourceFile);
|
||||
}
|
||||
}
|
||||
allFilesExcludingDefaultLibraryFile = result || emptyArray;
|
||||
return allFilesExcludingDefaultLibraryFile;
|
||||
|
||||
function addSourceFile(sourceFile: SourceFile) {
|
||||
if (!program.isSourceFileDefaultLibrary(sourceFile)) {
|
||||
(result || (result = [])).push(sourceFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When program emits non modular code, gets the files affected by the sourceFile whose shape has changed
|
||||
*/
|
||||
function getFilesAffectedByUpdatedShapeWhenNonModuleEmit(programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile) {
|
||||
const compilerOptions = programOfThisState.getCompilerOptions();
|
||||
// If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project,
|
||||
// so returning the file itself is good enough.
|
||||
if (compilerOptions && (compilerOptions.out || compilerOptions.outFile)) {
|
||||
return [sourceFileWithUpdatedShape];
|
||||
}
|
||||
return getAllFilesExcludingDefaultLibraryFile(programOfThisState, sourceFileWithUpdatedShape);
|
||||
}
|
||||
|
||||
/**
|
||||
* When program emits modular code, gets the files affected by the sourceFile whose shape has changed
|
||||
*/
|
||||
function getFilesAffectedByUpdatedShapeWhenModuleEmit(programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined) {
|
||||
if (!isExternalModule(sourceFileWithUpdatedShape) && !containsOnlyAmbientModules(sourceFileWithUpdatedShape)) {
|
||||
return getAllFilesExcludingDefaultLibraryFile(programOfThisState, sourceFileWithUpdatedShape);
|
||||
}
|
||||
|
||||
const compilerOptions = programOfThisState.getCompilerOptions();
|
||||
if (compilerOptions && (compilerOptions.isolatedModules || compilerOptions.out || compilerOptions.outFile)) {
|
||||
return [sourceFileWithUpdatedShape];
|
||||
}
|
||||
|
||||
// Now we need to if each file in the referencedBy list has a shape change as well.
|
||||
// Because if so, its own referencedBy files need to be saved as well to make the
|
||||
// emitting result consistent with files on disk.
|
||||
const seenFileNamesMap = createMap<SourceFile>();
|
||||
|
||||
// Start with the paths this file was referenced by
|
||||
seenFileNamesMap.set(sourceFileWithUpdatedShape.path, sourceFileWithUpdatedShape);
|
||||
const queue = getReferencedByPaths(sourceFileWithUpdatedShape.path);
|
||||
while (queue.length > 0) {
|
||||
const currentPath = queue.pop();
|
||||
if (!seenFileNamesMap.has(currentPath)) {
|
||||
const currentSourceFile = programOfThisState.getSourceFileByPath(currentPath);
|
||||
seenFileNamesMap.set(currentPath, currentSourceFile);
|
||||
if (currentSourceFile && updateShapeSignature(programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken)) {
|
||||
queue.push(...getReferencedByPaths(currentPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return array of values that needs emit
|
||||
return flatMapIter(seenFileNamesMap.values(), value => value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
namespace ts.BuilderState {
|
||||
type ComputeHash = (data: string) => string;
|
||||
|
||||
/**
|
||||
* Gets the files affected by the path from the program
|
||||
*/
|
||||
export function getFilesAffectedBy(state: BuilderState, programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, computeHash?: ComputeHash, cacheToUpdateSignature?: Map<string>): ReadonlyArray<SourceFile> {
|
||||
export function getFilesAffectedBy(state: BuilderState, programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, cacheToUpdateSignature?: Map<string>): ReadonlyArray<SourceFile> {
|
||||
// Since the operation could be cancelled, the signatures are always stored in the cache
|
||||
// They will be commited once it is safe to use them
|
||||
// eg when calling this api from tsserver, if there is no cancellation of the operation
|
||||
@@ -593,7 +203,7 @@ namespace ts.BuilderState {
|
||||
/**
|
||||
* Returns if the shape of the signature has changed since last emit
|
||||
*/
|
||||
function updateShapeSignature(state: Readonly<BuilderState>, programOfThisState: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash | undefined) {
|
||||
function updateShapeSignature(state: Readonly<BuilderState>, programOfThisState: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash) {
|
||||
Debug.assert(!!sourceFile);
|
||||
|
||||
// If we have cached the result for this file, that means hence forth we should assume file shape is uptodate
|
||||
@@ -612,7 +222,7 @@ namespace ts.BuilderState {
|
||||
else {
|
||||
const emitOutput = getFileEmitOutput(programOfThisState, sourceFile, /*emitOnlyDtsFiles*/ true, cancellationToken);
|
||||
if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
|
||||
latestSignature = (computeHash || identity)(emitOutput.outputFiles[0].text);
|
||||
latestSignature = computeHash(emitOutput.outputFiles[0].text);
|
||||
}
|
||||
else {
|
||||
latestSignature = prevSignature;
|
||||
@@ -623,6 +233,58 @@ namespace ts.BuilderState {
|
||||
return !prevSignature || latestSignature !== prevSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the dependencies of the sourceFile
|
||||
*/
|
||||
export function getAllDependencies(state: BuilderState, programOfThisState: Program, sourceFile: SourceFile): ReadonlyArray<string> {
|
||||
const compilerOptions = programOfThisState.getCompilerOptions();
|
||||
// With --out or --outFile all outputs go into single file, all files depend on each other
|
||||
if (compilerOptions.outFile || compilerOptions.out) {
|
||||
return getAllFileNames(state, programOfThisState);
|
||||
}
|
||||
|
||||
// If this is non module emit, or its a global file, it depends on all the source files
|
||||
if (!state.referencedMap || (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile))) {
|
||||
return getAllFileNames(state, programOfThisState);
|
||||
}
|
||||
|
||||
// Get the references, traversing deep from the referenceMap
|
||||
const seenMap = createMap<true>();
|
||||
const queue = [sourceFile.path];
|
||||
while (queue.length) {
|
||||
const path = queue.pop();
|
||||
if (!seenMap.has(path)) {
|
||||
seenMap.set(path, true);
|
||||
const references = state.referencedMap.get(path);
|
||||
if (references) {
|
||||
const iterator = references.keys();
|
||||
for (let { value, done } = iterator.next(); !done; { value, done } = iterator.next()) {
|
||||
queue.push(value as Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flatMapIter(seenMap.keys(), path => {
|
||||
const file = programOfThisState.getSourceFileByPath(path as Path);
|
||||
if (file) {
|
||||
return file.fileName;
|
||||
}
|
||||
return path;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the names of all files from the program
|
||||
*/
|
||||
function getAllFileNames(state: BuilderState, programOfThisState: Program): ReadonlyArray<string> {
|
||||
if (!state.allFileNames) {
|
||||
const sourceFiles = programOfThisState.getSourceFiles();
|
||||
state.allFileNames = sourceFiles === emptyArray ? emptyArray : sourceFiles.map(file => file.fileName);
|
||||
}
|
||||
return state.allFileNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the files referenced by the the file path
|
||||
*/
|
||||
|
||||
@@ -460,7 +460,7 @@ namespace ts.server {
|
||||
return [];
|
||||
}
|
||||
this.updateGraph();
|
||||
this.builderState = createBuilderState(this.program, this.projectService.toCanonicalFileName, this.builderState);
|
||||
this.builderState = BuilderState.create(this.program, this.projectService.toCanonicalFileName, this.builderState);
|
||||
return mapDefined(BuilderState.getFilesAffectedBy(this.builderState, this.program, scriptInfo.path, this.cancellationToken, data => this.projectService.host.createHash(data)),
|
||||
sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)) ? sourceFile.fileName : undefined);
|
||||
}
|
||||
|
||||
@@ -7217,7 +7217,7 @@ declare namespace ts.server {
|
||||
languageServiceEnabled: boolean;
|
||||
readonly trace?: (s: string) => void;
|
||||
readonly realpath?: (path: string) => string;
|
||||
private builder;
|
||||
private builderState;
|
||||
/**
|
||||
* Set of files names that were updated since the last call to getChangesSinceVersion.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user