Handle error reporting of files when new file is created after its opened in editor (#36271)

* If script info is not attached to the project on which wild card is invoked, update it.

* Instead of getting default project before starting error list timer, get it at that time if no project is specified
Fixes #35794

* Fix the open File watch triggered setting
This commit is contained in:
Sheetal Nandi 2020-01-24 16:07:48 -08:00 committed by GitHub
parent 8e0b091795
commit 096e1b12e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 182 additions and 25 deletions

View File

@ -1110,7 +1110,14 @@ namespace ts.server {
// don't trigger callback on open, existing files
if (project.fileIsOpen(fileOrDirectoryPath)) {
if (project.pendingReload !== ConfigFileProgramReloadLevel.Full) {
project.openFileWatchTriggered.set(fileOrDirectoryPath, true);
const info = Debug.assertDefined(this.getScriptInfoForPath(fileOrDirectoryPath));
if (info.isAttached(project)) {
project.openFileWatchTriggered.set(fileOrDirectoryPath, true);
}
else {
project.pendingReload = ConfigFileProgramReloadLevel.Partial;
this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project);
}
}
return;
}

View File

@ -719,10 +719,8 @@ namespace ts.server {
this.projectService.logger.info(`got projects updated in background, updating diagnostics for ${openFiles}`);
if (openFiles.length) {
if (!this.suppressDiagnosticEvents && !this.noGetErrOnBackgroundUpdate) {
const checkList = this.createCheckList(openFiles);
// 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));
this.errorCheck.startNew(next => this.updateErrorCheck(next, openFiles, 100, /*requireOpen*/ true));
}
// Send project changed event
@ -870,20 +868,37 @@ namespace ts.server {
}
/** It is the caller's responsibility to verify that `!this.suppressDiagnosticEvents`. */
private updateErrorCheck(next: NextStep, checkList: PendingErrorCheck[], ms: number, requireOpen = true) {
private updateErrorCheck(next: NextStep, checkList: readonly string[] | readonly PendingErrorCheck[], ms: number, requireOpen = true) {
Debug.assert(!this.suppressDiagnosticEvents); // Caller's responsibility
const seq = this.changeSeq;
const followMs = Math.min(ms, 200);
let index = 0;
const goNext = () => {
index++;
if (checkList.length > index) {
next.delay(followMs, checkOne);
}
};
const checkOne = () => {
if (this.changeSeq !== seq) {
return;
}
const { fileName, project } = checkList[index];
index++;
let item: string | PendingErrorCheck | undefined = checkList[index];
if (isString(item)) {
// Find out project for the file name
item = this.toPendingErrorCheck(item);
if (!item) {
// Ignore file if there is no project for the file
goNext();
return;
}
}
const { fileName, project } = item;
// Ensure the project is upto date before checking if this file is present in the project
updateProjectIfDirty(project);
if (!project.containsFile(fileName, requireOpen)) {
@ -901,11 +916,6 @@ namespace ts.server {
return;
}
const goNext = () => {
if (checkList.length > index) {
next.delay(followMs, checkOne);
}
};
if (this.getPreferences(fileName).disableSuggestions) {
goNext();
}
@ -1727,12 +1737,10 @@ namespace ts.server {
}
}
private createCheckList(fileNames: string[]): PendingErrorCheck[] {
return mapDefined<string, PendingErrorCheck>(fileNames, uncheckedFileName => {
const fileName = toNormalizedPath(uncheckedFileName);
const project = this.projectService.tryGetDefaultProjectForFile(fileName);
return project && { fileName, project };
});
private toPendingErrorCheck(uncheckedFileName: string): PendingErrorCheck | undefined {
const fileName = toNormalizedPath(uncheckedFileName);
const project = this.projectService.tryGetDefaultProjectForFile(fileName);
return project && { fileName, project };
}
private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void {
@ -1740,9 +1748,8 @@ namespace ts.server {
return;
}
const checkList = this.createCheckList(fileNames);
if (checkList.length > 0) {
this.updateErrorCheck(next, checkList, delay);
if (fileNames.length > 0) {
this.updateErrorCheck(next, fileNames, delay);
}
}

View File

@ -84,7 +84,10 @@ namespace ts.projectSystem {
command: "geterr",
arguments: { files: ["/a/missing"] }
});
// no files - expect 'completed' event
// Queued files
assert.equal(host.getOutput().length, 0, "expected 0 message");
host.checkTimeoutQueueLengthAndRun(1);
// Completed event since file is missing
assert.equal(host.getOutput().length, 1, "expect 1 message");
verifyRequestCompleted(session.getSeq(), 0);
}

View File

@ -923,6 +923,146 @@ declare var console: {
const service = createProjectService(host);
service.openClientFile(file.path);
});
describe("when creating new file", () => {
const foo: File = {
path: `${tscWatch.projectRoot}/src/foo.ts`,
content: "export function foo() { }"
};
const bar: File = {
path: `${tscWatch.projectRoot}/src/bar.ts`,
content: "export function bar() { }"
};
const config: File = {
path: `${tscWatch.projectRoot}/tsconfig.json`,
content: JSON.stringify({
include: ["./src"]
})
};
const fooBar: File = {
path: `${tscWatch.projectRoot}/src/sub/fooBar.ts`,
content: "export function fooBar() { }"
};
function verifySessionWorker({ withExclude, openFileBeforeCreating, checkProjectBeforeError, checkProjectAfterError, }: VerifySession, errorOnNewFileBeforeOldFile: boolean) {
const host = createServerHost([
foo, bar, libFile, { path: `${tscWatch.projectRoot}/src/sub` },
withExclude ?
{
path: config.path,
content: JSON.stringify({
include: ["./src"],
exclude: ["./src/sub"]
})
} :
config
]);
const session = createSession(host, {
canUseEvents: true
});
session.executeCommandSeq<protocol.OpenRequest>({
command: protocol.CommandTypes.Open,
arguments: {
file: foo.path,
fileContent: foo.content,
projectRootPath: tscWatch.projectRoot
}
});
if (!openFileBeforeCreating) {
host.writeFile(fooBar.path, fooBar.content);
}
session.executeCommandSeq<protocol.OpenRequest>({
command: protocol.CommandTypes.Open,
arguments: {
file: fooBar.path,
fileContent: fooBar.content,
projectRootPath: tscWatch.projectRoot
}
});
if (openFileBeforeCreating) {
host.writeFile(fooBar.path, fooBar.content);
}
const service = session.getProjectService();
checkProjectBeforeError(service);
verifyGetErrRequest({
session,
host,
expected: errorOnNewFileBeforeOldFile ?
[
{ file: fooBar, syntax: [], semantic: [], suggestion: [] },
{ file: foo, syntax: [], semantic: [], suggestion: [] },
] :
[
{ file: foo, syntax: [], semantic: [], suggestion: [] },
{ file: fooBar, syntax: [], semantic: [], suggestion: [] },
],
existingTimeouts: 2
});
checkProjectAfterError(service);
}
interface VerifySession {
withExclude?: boolean;
openFileBeforeCreating: boolean;
checkProjectBeforeError: (service: server.ProjectService) => void;
checkProjectAfterError: (service: server.ProjectService) => void;
}
function verifySession(input: VerifySession) {
it("when error on new file are asked before old one", () => {
verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ true);
});
it("when error on new file are asked after old one", () => {
verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ false);
});
}
function checkFooBarInInferredProject(service: server.ProjectService) {
checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 });
checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, libFile.path, config.path]);
checkProjectActualFiles(service.inferredProjects[0], [fooBar.path, libFile.path]);
}
function checkFooBarInConfiguredProject(service: server.ProjectService) {
checkNumberOfProjects(service, { configuredProjects: 1 });
checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, fooBar.path, libFile.path, config.path]);
}
describe("when new file creation directory watcher is invoked before file is opened in editor", () => {
verifySession({
openFileBeforeCreating: false,
checkProjectBeforeError: checkFooBarInConfiguredProject,
checkProjectAfterError: checkFooBarInConfiguredProject
});
describe("when new file is excluded from config", () => {
verifySession({
withExclude: true,
openFileBeforeCreating: false,
checkProjectBeforeError: checkFooBarInInferredProject,
checkProjectAfterError: checkFooBarInInferredProject
});
});
});
describe("when new file creation directory watcher is invoked after file is opened in editor", () => {
verifySession({
openFileBeforeCreating: true,
checkProjectBeforeError: checkFooBarInInferredProject,
checkProjectAfterError: service => {
// Both projects exist but fooBar is in configured project after the update
// Inferred project is yet to be updated so still has fooBar
checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 });
checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, fooBar.path, libFile.path, config.path]);
checkProjectActualFiles(service.inferredProjects[0], [fooBar.path, libFile.path]);
assert.isTrue(service.inferredProjects[0].dirty);
assert.equal(service.inferredProjects[0].getRootFilesMap().size, 0);
}
});
describe("when new file is excluded from config", () => {
verifySession({
withExclude: true,
openFileBeforeCreating: true,
checkProjectBeforeError: checkFooBarInInferredProject,
checkProjectAfterError: checkFooBarInInferredProject
});
});
});
});
});
describe("unittests:: tsserver:: ConfiguredProjects:: non-existing directories listed in config file input array", () => {

View File

@ -382,7 +382,7 @@ namespace ts.projectSystem {
}
});
host.runQueuedImmediateCallbacks();
host.checkTimeoutQueueLengthAndRun(1);
assert.isFalse(hasError());
checkCompleteEvent(session, 1, expectedSequenceId);
session.clearMessages();

View File

@ -1508,7 +1508,7 @@ var x = 10;`
host,
expected: [
{ file: fileB, syntax: [], semantic: [], suggestion: [] },
{ file: fileSubA },
{ file: fileSubA, syntax: [], semantic: [], suggestion: [] },
],
existingTimeouts: 2,
onErrEvent: () => assert.isFalse(hasErrorMsg())

View File

@ -9483,7 +9483,7 @@ declare namespace ts.server {
private getCompileOnSaveAffectedFileList;
private emitFile;
private getSignatureHelpItems;
private createCheckList;
private toPendingErrorCheck;
private getDiagnostics;
private change;
private reload;