Add api in builder to get changed files and use it to send project changed event

This commit is contained in:
Sheetal Nandi
2017-08-15 14:50:52 -07:00
parent e65df125f8
commit e71123857c
6 changed files with 224 additions and 142 deletions

View File

@@ -18,6 +18,13 @@ namespace ts {
text: string;
}
export interface ChangedProgramFiles {
/** Minimal set of list of files that require emit */
readonly filesToEmit: ReadonlyArray<string>;
/** File paths of source files changed/added/removed or affected by changed files */
readonly changedFiles: ReadonlyArray<string>;
}
export interface Builder {
/**
* This is the callback when file infos in the builder are updated
@@ -25,8 +32,15 @@ namespace ts {
onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution): void;
getFilesAffectedBy(program: Program, path: Path): string[];
emitFile(program: Program, path: Path): EmitOutput;
/** Emit the changed files and clear the cache of the changed files */
emitChangedFiles(program: Program): EmitOutputDetailed[];
/** Get the changed files since last query and then clear the cache of changed files */
getChangedProgramFiles(program: Program): ChangedProgramFiles;
/** When called gets the semantic diagnostics for the program. It also caches the diagnostics and manage them */
getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[];
/** Called to reset the status of the builder */
clear(): void;
}
@@ -73,16 +87,18 @@ namespace ts {
): Builder {
let isModuleEmit: boolean | undefined;
// Last checked shape signature for the file info
type FileInfo = { version: string; signature: string; };
let fileInfos: Map<FileInfo>;
type FileInfo = { fileName: string; version: string; signature: string; };
const fileInfos = createMap<FileInfo>();
const semanticDiagnosticsPerFile = createMap<Diagnostic[]>();
let changedFilesSinceLastEmit: Map<true>;
/** The map has key by source file's path that has been changed */
const changedFileNames = createMap<string>();
let emitHandler: EmitHandler;
return {
onProgramUpdateGraph,
getFilesAffectedBy,
emitFile,
emitChangedFiles,
getChangedProgramFiles,
getSemanticDiagnostics,
clear
};
@@ -92,13 +108,11 @@ namespace ts {
if (isModuleEmit !== currentIsModuleEmit) {
isModuleEmit = currentIsModuleEmit;
emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler();
fileInfos = undefined;
fileInfos.clear();
semanticDiagnosticsPerFile.clear();
}
changedFilesSinceLastEmit = changedFilesSinceLastEmit || createMap<true>();
mutateMap(
fileInfos || (fileInfos = createMap()),
fileInfos,
arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path),
{
// Add new file info
@@ -111,27 +125,26 @@ namespace ts {
);
}
function registerChangedFile(path: Path) {
changedFilesSinceLastEmit.set(path, true);
function registerChangedFile(path: Path, fileName: string) {
changedFileNames.set(path, fileName);
// All changed files need to re-evaluate its semantic diagnostics
semanticDiagnosticsPerFile.delete(path);
}
function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo {
registerChangedFile(sourceFile.path);
registerChangedFile(sourceFile.path, sourceFile.fileName);
emitHandler.addScriptInfo(program, sourceFile);
return { version: sourceFile.version, signature: undefined };
return { fileName: sourceFile.fileName, version: sourceFile.version, signature: undefined };
}
function removeExistingFileInfo(path: Path, _existingFileInfo: FileInfo) {
registerChangedFile(path);
function removeExistingFileInfo(path: Path, existingFileInfo: FileInfo) {
registerChangedFile(path, existingFileInfo.fileName);
emitHandler.removeScriptInfo(path);
}
function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) {
if (existingInfo.version !== sourceFile.version || hasInvalidatedResolution(sourceFile.path)) {
registerChangedFile(sourceFile.path);
semanticDiagnosticsPerFile.delete(sourceFile.path);
registerChangedFile(sourceFile.path, sourceFile.fileName);
existingInfo.version = sourceFile.version;
emitHandler.updateScriptInfo(program, sourceFile);
}
@@ -154,7 +167,7 @@ namespace ts {
const sourceFile = program.getSourceFile(path);
const singleFileResult = sourceFile && shouldEmitFile(sourceFile) ? [sourceFile.fileName] : [];
const info = fileInfos && fileInfos.get(path);
const info = fileInfos.get(path);
if (!info || !updateShapeSignature(program, sourceFile, info)) {
return singleFileResult;
}
@@ -165,44 +178,61 @@ namespace ts {
function emitFile(program: Program, path: Path) {
ensureProgramGraph(program);
if (!fileInfos || !fileInfos.has(path)) {
if (!fileInfos.has(path)) {
return { outputFiles: [], emitSkipped: true };
}
return getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false, /*isDetailed*/ false);
}
function emitChangedFiles(program: Program): EmitOutputDetailed[] {
ensureProgramGraph(program);
const result: EmitOutputDetailed[] = [];
if (changedFilesSinceLastEmit) {
const seenFiles = createMap<SourceFile>();
changedFilesSinceLastEmit.forEach((__value, path: Path) => {
const affectedFiles = getFilesAffectedBy(program, path);
for (const file of affectedFiles) {
if (!seenFiles.has(file)) {
const sourceFile = program.getSourceFile(file);
seenFiles.set(file, sourceFile);
if (sourceFile) {
// Any affected file shouldnt have the cached diagnostics
semanticDiagnosticsPerFile.delete(sourceFile.path);
function enumerateChangedFilesSet(
program: Program,
onChangedFile: (fileName: string) => void,
onAffectedFile: (fileName: string, sourceFile: SourceFile) => void
) {
changedFileNames.forEach((fileName, path) => {
onChangedFile(fileName);
const affectedFiles = getFilesAffectedBy(program, path as Path);
for (const file of affectedFiles) {
onAffectedFile(file, program.getSourceFile(file));
}
});
}
const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ false, /*isDetailed*/ true) as EmitOutputDetailed;
result.push(emitOutput);
function enumerateChangedFilesEmitOutput(
program: Program,
emitOnlyDtsFiles: boolean,
onChangedFile: (fileName: string) => void,
onEmitOutput: (emitOutput: EmitOutputDetailed, sourceFile: SourceFile) => void
) {
const seenFiles = createMap<SourceFile>();
enumerateChangedFilesSet(program, onChangedFile, (fileName, sourceFile) => {
if (!seenFiles.has(fileName)) {
seenFiles.set(fileName, sourceFile);
if (sourceFile) {
// Any affected file shouldnt have the cached diagnostics
semanticDiagnosticsPerFile.delete(sourceFile.path);
// mark all the emitted source files as seen
if (emitOutput.emittedSourceFiles) {
for (const file of emitOutput.emittedSourceFiles) {
seenFiles.set(file.fileName, file);
}
}
const emitOutput = getEmitOutput(program, sourceFile, emitOnlyDtsFiles, /*isDetailed*/ true) as EmitOutputDetailed;
onEmitOutput(emitOutput, sourceFile);
// mark all the emitted source files as seen
if (emitOutput.emittedSourceFiles) {
for (const file of emitOutput.emittedSourceFiles) {
seenFiles.set(file.fileName, file);
}
}
}
});
}
});
}
changedFilesSinceLastEmit = undefined;
}
function emitChangedFiles(program: Program): EmitOutputDetailed[] {
ensureProgramGraph(program);
const result: EmitOutputDetailed[] = [];
enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ false,
/*onChangedFile*/ noop, emitOutput => result.push(emitOutput));
changedFileNames.clear();
return result;
}
@@ -210,17 +240,11 @@ namespace ts {
ensureProgramGraph(program);
// Ensure that changed files have cleared their respective
if (changedFilesSinceLastEmit) {
changedFilesSinceLastEmit.forEach((__value, path: Path) => {
const affectedFiles = getFilesAffectedBy(program, path);
for (const file of affectedFiles) {
const sourceFile = program.getSourceFile(file);
if (sourceFile) {
semanticDiagnosticsPerFile.delete(sourceFile.path);
}
}
});
}
enumerateChangedFilesSet(program, /*onChangedFile*/ noop, (_affectedFileName, sourceFile) => {
if (sourceFile) {
semanticDiagnosticsPerFile.delete(sourceFile.path);
}
});
let diagnostics: Diagnostic[];
for (const sourceFile of program.getSourceFiles()) {
@@ -240,11 +264,36 @@ namespace ts {
return diagnostics || emptyArray;
}
function getChangedProgramFiles(program: Program): ChangedProgramFiles {
ensureProgramGraph(program);
let filesToEmit: string[];
let changedFiles: string[];
enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ true,
// All the changed files are required to get diagnostics
changedFileName => addFileForDiagnostics(changedFileName),
// Emitted file is for emit as well as diagnostic
(_emitOutput, sourceFile) => {
(filesToEmit || (filesToEmit = [])).push(sourceFile.fileName);
addFileForDiagnostics(sourceFile.fileName);
});
changedFileNames.clear();
return {
filesToEmit: filesToEmit || emptyArray,
changedFiles: changedFiles || emptyArray
};
function addFileForDiagnostics(fileName: string) {
(changedFiles || (changedFiles = [])).push(fileName);
}
}
function clear() {
isModuleEmit = undefined;
emitHandler = undefined;
fileInfos = undefined;
fileInfos.clear();
semanticDiagnosticsPerFile.clear();
changedFileNames.clear();
}
/**
@@ -287,9 +336,7 @@ namespace ts {
}
/**
* Gets the referenced files for a file from the program
* @param program
* @param path
* Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true
*/
function getReferencedFiles(program: Program, sourceFile: SourceFile): Map<true> {
const referencedFiles = createMap<true>();
@@ -371,55 +418,36 @@ namespace ts {
function getModuleEmitHandler(): EmitHandler {
const references = createMap<Map<true>>();
const referencedBy = createMultiMap<Path>();
return {
addScriptInfo: (program, sourceFile) => {
const refs = createMap<true>();
references.set(sourceFile.path, refs);
setReferences(program, sourceFile, refs);
},
addScriptInfo: setReferences,
removeScriptInfo,
updateScriptInfo: (program, sourceFile) => setReferences(program, sourceFile, references.get(sourceFile.path)),
updateScriptInfo: setReferences,
getFilesAffectedByUpdatedShape
};
function setReferences(program: Program, sourceFile: SourceFile, existingReferences: Map<true>) {
const path = sourceFile.path;
mutateMap(
// Existing references
existingReferences,
// Updated references
getReferencedFiles(program, sourceFile),
{
// Creating new Reference: as sourceFile references file with path 'key'
// in other words source file (path) is referenced by 'key'
createNewValue: (key): true => { referencedBy.add(key, path); return true; },
// Remove existing reference by entry: source file doesnt reference file 'key' any more
// in other words source file (path) is not referenced by 'key'
onDeleteValue: (key, _existingValue) => { referencedBy.remove(key, path); }
}
);
function setReferences(program: Program, sourceFile: SourceFile) {
references.set(sourceFile.path, getReferencedFiles(program, sourceFile));
}
function removeScriptInfo(path: Path) {
function removeScriptInfo(removedFilePath: Path) {
// Remove existing references
references.forEach((_value, key) => {
referencedBy.remove(key, path);
});
references.delete(path);
// Delete the entry and add files referencing this file, as chagned files too
const referencedByPaths = referencedBy.get(path);
if (referencedByPaths) {
for (const path of referencedByPaths) {
registerChangedFile(path);
references.forEach((referencesInFile, filePath) => {
if (referencesInFile.has(removedFilePath)) {
// add files referencing the removedFilePath, as changed files too
const referencedByInfo = fileInfos.get(filePath);
if (referencedByInfo) {
registerChangedFile(filePath as Path, referencedByInfo.fileName);
}
}
referencedBy.delete(path);
}
});
// Delete the entry for the removed file path
references.delete(removedFilePath);
}
function getReferencedByPaths(path: Path) {
return referencedBy.get(path) || [];
function getReferencedByPaths(referencedFilePath: Path) {
return mapDefinedIter(references.entries(), ([filePath, referencesInFile]) =>
referencesInFile.has(referencedFilePath) ? filePath as Path : undefined
);
}
function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] {
@@ -444,7 +472,7 @@ namespace ts {
// Start with the paths this file was referenced by
const path = sourceFile.path;
setSeenFileName(path, sourceFile);
const queue = getReferencedByPaths(path).slice();
const queue = getReferencedByPaths(path);
while (queue.length > 0) {
const currentPath = queue.pop();
if (!seenFileNamesMap.has(currentPath)) {

View File

@@ -125,7 +125,7 @@ namespace ts.projectSystem {
this.events.push(event);
}
checkEventCountOfType(eventType: "context" | "configFileDiag", expectedCount: number) {
checkEventCountOfType(eventType: "configFileDiag", expectedCount: number) {
const eventsOfType = filter(this.events, e => e.eventName === eventType);
assert.equal(eventsOfType.length, expectedCount, `The actual event counts of type ${eventType} is ${eventsOfType.length}, while expected ${expectedCount}`);
}
@@ -2001,7 +2001,7 @@ namespace ts.projectSystem {
const session = createSession(host, {
canUseEvents: true,
eventHandler: e => {
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ContextEvent || e.eventName === server.ProjectInfoTelemetryEvent) {
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectChangedEvent || e.eventName === server.ProjectInfoTelemetryEvent) {
return;
}
assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent);

View File

@@ -10,14 +10,14 @@
namespace ts.server {
export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024;
export const ContextEvent = "context";
export const ProjectChangedEvent = "projectChanged";
export const ConfigFileDiagEvent = "configFileDiag";
export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState";
export const ProjectInfoTelemetryEvent = "projectInfo";
export interface ContextEvent {
eventName: typeof ContextEvent;
data: { project: Project; fileName: NormalizedPath };
export interface ProjectChangedEvent {
eventName: typeof ProjectChangedEvent;
data: { project: Project; filesToEmit: string[]; changedFiles: string[] };
}
export interface ConfigFileDiagEvent {
@@ -77,7 +77,7 @@ namespace ts.server {
readonly dts: number;
}
export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent;
export type ProjectServiceEvent = ProjectChangedEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent;
export interface ProjectServiceEventHandler {
(event: ProjectServiceEvent): void;
@@ -492,9 +492,32 @@ namespace ts.server {
if (this.pendingProjectUpdates.delete(projectName)) {
project.updateGraph();
}
// Send the update event to notify about the project changes
this.sendProjectChangedEvent(project);
});
}
private sendProjectChangedEvent(project: Project) {
if (project.isClosed() || !this.eventHandler || !project.languageServiceEnabled) {
return;
}
const { filesToEmit, changedFiles } = project.getChangedFiles();
if (changedFiles.length === 0) {
return;
}
const event: ProjectChangedEvent = {
eventName: ProjectChangedEvent,
data: {
project,
filesToEmit: filesToEmit as string[],
changedFiles: changedFiles as string[]
}
};
this.eventHandler(event);
}
/* @internal */
delayUpdateProjectGraphAndInferredProjectsRefresh(project: Project) {
this.delayUpdateProjectGraph(project);
@@ -678,19 +701,6 @@ namespace ts.server {
// update projects to make sure that set of referenced files is correct
this.delayUpdateProjectGraphs(containingProjects);
// TODO: (sheetalkamat) Someway to send this event so that error checks are updated?
// if (!this.eventHandler) {
// return;
// }
// for (const openFile of this.openFiles) {
// const event: ContextEvent = {
// eventName: ContextEvent,
// data: { project: openFile.getDefaultProject(), fileName: openFile.fileName }
// };
// this.eventHandler(event);
// }
}
}

View File

@@ -133,7 +133,7 @@ namespace ts.server {
/*@internal*/
lsHost: LSHost;
builder: Builder;
private builder: Builder;
/**
* Set of files names that were updated since the last call to getChangesSinceVersion.
*/
@@ -307,6 +307,12 @@ namespace ts.server {
return !emitSkipped;
}
getChangedFiles() {
Debug.assert(this.languageServiceEnabled);
this.ensureBuilder();
return this.builder.getChangedProgramFiles(this.program);
}
getProjectVersion() {
return this.projectStateVersion.toString();
}
@@ -392,6 +398,10 @@ namespace ts.server {
this.languageService = undefined;
}
isClosed() {
return this.lsHost === undefined;
}
getCompilerOptions() {
return this.compilerOptions;
}
@@ -635,8 +645,9 @@ namespace ts.server {
// update builder only if language service is enabled
// otherwise tell it to drop its internal state
// Note we are retaining builder so we can send events for project change
if (this.builder) {
if (this.languageServiceEnabled && this.compileOnSaveEnabled) {
if (this.languageServiceEnabled) {
this.builder.onProgramUpdateGraph(this.program, this.lsHost.hasInvalidatedResolution);
}
else {

View File

@@ -2040,6 +2040,29 @@ namespace ts.server.protocol {
languageServiceEnabled: boolean;
}
export type ProjectChangedEventName = "projectChanged";
export interface ProjectStructureChangedEvent extends Event {
event: ProjectChangedEventName;
body: ProjectChangedEventBody;
}
export interface ProjectChangedEventBody {
/**
* Project name that has changes
*/
projectName: string;
/**
* Minimum set of file names to emit
*/
fileNamesToEmit: string[];
/**
* List of files that have changed/added/removed or could have been affected by the changed files
*/
changedFiles: string[];
}
/**
* Arguments for reload request.
*/

View File

@@ -332,14 +332,18 @@ namespace ts.server {
private defaultEventHandler(event: ProjectServiceEvent) {
switch (event.eventName) {
case ContextEvent:
const { project, fileName } = event.data;
this.projectService.logger.info(`got context event, updating diagnostics for ${fileName}`);
this.errorCheck.startNew(next => this.updateErrorCheck(next, [{ fileName, project }], 100));
case ProjectChangedEvent:
const { project, filesToEmit, changedFiles } = event.data;
this.projectChangedEvent(project, filesToEmit, changedFiles);
break;
case ConfigFileDiagEvent:
const { triggerFile, configFileName, diagnostics } = event.data;
this.configFileDiagnosticEvent(triggerFile, configFileName, diagnostics);
const { triggerFile, configFileName: configFile, diagnostics } = event.data;
const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true));
this.event<protocol.ConfigFileDiagnosticEventBody>({
triggerFile,
configFile,
diagnostics: bakedDiags
}, "configFileDiag");
break;
case ProjectLanguageServiceStateEvent: {
const eventName: protocol.ProjectLanguageServiceStateEventName = "projectLanguageServiceState";
@@ -360,6 +364,24 @@ namespace ts.server {
}
}
private projectChangedEvent(project: Project, fileNamesToEmit: string[], changedFiles: string[]): void {
this.projectService.logger.info(`got project changed event, updating diagnostics for ${changedFiles}`);
if (changedFiles.length) {
const checkList = this.createCheckList(changedFiles, project);
// For now only queue error checking for open files. We can change this to include non open files as well
this.errorCheck.startNew(next => this.updateErrorCheck(next, checkList, 100, /*requireOpen*/ true));
// Send project changed event
this.event<protocol.ProjectChangedEventBody>({
projectName: project.getProjectName(),
changedFiles,
fileNamesToEmit
}, "projectChanged");
}
}
public logError(err: Error, cmd: string) {
let msg = "Exception on executing command " + cmd;
if (err.message) {
@@ -381,21 +403,6 @@ namespace ts.server {
this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine));
}
public configFileDiagnosticEvent(triggerFile: string, configFile: string, diagnostics: ReadonlyArray<Diagnostic>) {
const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true));
const ev: protocol.ConfigFileDiagnosticEvent = {
seq: 0,
type: "event",
event: "configFileDiag",
body: {
triggerFile,
configFile,
diagnostics: bakedDiags
}
};
this.send(ev);
}
public event<T>(info: T, eventName: string) {
const ev: protocol.Event = {
seq: 0,
@@ -1257,13 +1264,16 @@ namespace ts.server {
}
}
private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void {
const checkList = mapDefined<string, PendingErrorCheck>(fileNames, uncheckedFileName => {
private createCheckList(fileNames: string[], defaultProject?: Project): PendingErrorCheck[] {
return mapDefined<string, PendingErrorCheck>(fileNames, uncheckedFileName => {
const fileName = toNormalizedPath(uncheckedFileName);
const project = this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true);
const project = defaultProject || this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true);
return project && { fileName, project };
});
}
private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void {
const checkList = this.createCheckList(fileNames);
if (checkList.length > 0) {
this.updateErrorCheck(next, checkList, delay);
}