Skip to content

Watch for the creation of missing files#16684

Merged
amcasey merged 7 commits into
microsoft:masterfrom
amcasey:Vsts434619
Jun 29, 2017
Merged

Watch for the creation of missing files#16684
amcasey merged 7 commits into
microsoft:masterfrom
amcasey:Vsts434619

Conversation

@amcasey
Copy link
Copy Markdown
Member

@amcasey amcasey commented Jun 21, 2017

When we discover that a file is missing, call watchFile on its path so that we can update our state when it is created.

  1. For tsc --watch, we schedule a new build
  2. For tsserver, we update the containing projects

Caveat: VS will not know about the state change in tsserver until the next time it synchronizes its project list

@amcasey amcasey requested review from a user, rbuckton and sheetalkamat June 21, 2017 18:04
@msftclas
Copy link
Copy Markdown

@amcasey,
Thanks for your contribution as a Microsoft full-time employee or intern. You do not need to sign a CLA.
Thanks,
Microsoft Pull Request Bot

@amcasey
Copy link
Copy Markdown
Member Author

amcasey commented Jun 21, 2017

This PR supersedes #16527.

@amcasey amcasey changed the title Vsts434619 Watch for the creation of missing files Jun 21, 2017
Comment thread src/compiler/types.ts
/**
* Get a list of file names that were passed to 'createProgram' or referenced in a
* program source file but could not be located.
*/
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be optional.

Comment thread src/compiler/program.ts Outdated
// will be created until we encounter a change that prevents complete structure reuse.
// During this interval, creation of the file will go unnoticed. We expect this to be
// both rare and low-impact.
const missingFilePaths: Path[] = oldProgram.getMissingFilePaths() || emptyArray;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like getMissingFilePaths already never returns undefined. (Let's keep it that way.) Inline this declaration.

Comment thread src/compiler/program.ts
}

for (const p of oldProgram.getMissingFilePaths()) {
filesByName.set(p, undefined);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type of filesByName should indicate that this can be undefined.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to update the type, but I should mention that the map already had undefined values (this change just ensures that they persist across structure reuse).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To confirm, you're suggesting const filesByName = createFileMap<SourceFile | undefined>(); ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't appear to have any effect on the inferred type. Presumably, it would matter with strict null-checking enabled?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's just documentation for now.

Comment thread src/compiler/sys.ts
@@ -340,11 +340,20 @@ namespace ts {
}

function fileChanged(curr: any, prev: any) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was this way before, but it would be nice to remove any and include a type declaration saying what you expect this to be.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be interested to learn how to do this. Do I just specify that it has to have an mtime property or is there something more?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After getting offline help, I don't think this is worth fixing as part of this change.

Comment thread src/compiler/tsc.ts Outdated
const missingPaths = compileResult.program.getMissingFilePaths() || [];
missingPaths.forEach((path: Path): void => {
const fileWatcher = sys.watchFile(path, (_fileName: string, removed?: boolean) => {
// removed = deleted ? true : (added ? false : undefined)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use an enum rather than true | false | undefined. true -> E.Deleted, false -> E.Added, undefined -> E.Changed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was under the impression that we didn't control the signature, but I'll confirm.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread src/compiler/types.ts
/**
* Get a list of file names that were passed to 'createProgram' or referenced in a
* program source file but could not be located.
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would make this internal. (/* @internal */)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

const referenceFile: Harness.Compiler.TestFile = {
unitName: referenceFileName,
content:
"/// <reference path=\"d:/imaginary/nonexistent1.ts\"/>\n" + // Absolute
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to test all the different kinds of paths here? Also, would include a reference to an existing file for contrast.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I have a test that mixes existent and non-existent paths, but I'll confirm.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do have such a test, but it is for root files. I thought that duplicating all of the tests for triple-slash references would be redundant (and add to maintenance and runtime costs), so I skipped them. Let me know if you feel strongly and I'll make a second set of tests.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, should be fine then.

it("handles missing root file", () => {
const program = createProgram(["./nonexistent.ts"], options, testCompilerHost);
const missing = program.getMissingFilePaths();
assert.isDefined(missing);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use assert.deepEqual instead of asserting the length and elements separately.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for assert.isDefined since you're about to assert that it's equal to a defined value.

Comment thread src/server/project.ts Outdated
private rootFilesMap: FileMap<ScriptInfo> = createFileMap<ScriptInfo>();
private program: ts.Program;
private externalFiles: SortedReadonlyArray<string>;
private missingFilesMap: FileMap<FileWatcher> = createFileMap<FileWatcher>();
Copy link
Copy Markdown

@ghost ghost Jun 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need a FileMap, just a Map. Then you can iterate using forEach. (#16715)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't I need a FileMap if the key type is Path?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Path is a subtype of string, so you can use those as map keys. (It might be slightly more type safe to specify that keys must be Paths, but I don't think that's worth using a FileMap.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand when we use Path and FileMap. Do you have some time to explain?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I've made the change.

Comment thread src/server/project.ts Outdated
// Clean up file watchers waiting for missing files
for (const p of this.missingFilesMap.getKeys()) {
this.missingFilesMap.get(p).close();
this.missingFilesMap.remove(p);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of removing items one-by-one if you just set this.missingFileMap = undefined; at the end?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I cleared the map as an afterthought.

Comment thread src/server/project.ts Outdated
}
}

if (hasChanges) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was this way before, but I would prefer const hasChanges = ...; over `let hasChanges = false; if (...) { hasChanges = true; }

Comment thread src/server/project.ts Outdated
}

if (hasChanges) {
const missingFilePaths = this.program.getMissingFilePaths() || emptyArray;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, getMissingFilePaths should always be defined, so no need for || emptyArray.

Comment thread src/server/project.ts Outdated

if (hasChanges) {
const missingFilePaths = this.program.getMissingFilePaths() || emptyArray;
const missingFilePathsSet = createMap<true>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arrayToMap(missingFilePaths, path => path);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that have an advantage other than brevity?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it should have an identical result.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to result in a String-Path map, rather than a Path-true map.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should probably be a helper for this:

export function arrayToSet(array: string[]): Map<boolean> {
    return arrayToMap(array, key => key, () => true);
}

export function getDocumentHighlights(program: Program, cancellationToken: CancellationToken, sourceFile: SourceFile, position: number, sourceFilesToSearch: SourceFile[]): DocumentHighlights[] | undefined {
const node = getTouchingWord(sourceFile, position, /*includeJsDocComment*/ true);
if (!node) return undefined;
// Note that getTouchingWord indicates failure by returning the sourceFile node.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be its own PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a separate commit, so it would be no extra work to make it a separate PR. What's the local convention?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ask around?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find strong support for pulling it out. If it were more contentious, I would.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a test that was breaking under the old behavior?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, otherwise the code would have been correct. 😉

Comment thread src/server/project.ts Outdated
});

// Missing files that are not yet watched should be added to the map.
missingFilePaths.forEach(p => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a memorable variable name for p, like missingPath.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not in the habit of giving detailed names to iteration variables, but sure.

@amcasey
Copy link
Copy Markdown
Member Author

amcasey commented Jun 27, 2017

@Andy-MS I think I've addressed all of your feedback, other than the ones marked with ❓.

Comment thread src/compiler/core.ts Outdated
*
* @param array the array of input elements.
*
* This function makes no effort to avoid collisions; if any two elements produce
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a Map<true>, so the elements of the array aren't present in the result. So the element with the higher index in the array will be the one associated with the produced key can't be true.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted.

Comment thread src/server/project.ts Outdated
// Files that are no longer missing (e.g. because they are no longer required)
// should no longer be watched.
for (const missingFilePath of this.missingFilesMap.getKeys())
const missingFilesToDelete: string[] = [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be safe to delete values from a map while iterating over it. Are you sure this was the cause of an error?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I just couldn't find any documentation indicating that it was safe.

@amcasey
Copy link
Copy Markdown
Member Author

amcasey commented Jun 28, 2017

After a high-level review from @mhegazy and a detailed review from @Andy-MS, I think this is ready to be merged. Any final comments? (cc: @rbuckton)

Comment thread src/compiler/program.ts Outdated
}
}

const missingFilePaths = filesByName.getKeys().filter(p => !filesByName.get(p));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will have a semantic conflict with #16724 as filesByName will be a Map, which doesn't have a getKeys(). You can use mapDefined<[string, string], string>(arrayFrom(m.entries()), ([path, value]) => value === undefined ? undefined : path);. Or add a version of mapDefined that works on iterators to avoid the arrayFrom.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rebase on top of your commit.

Comment thread src/compiler/program.ts Outdated
// will be created until we encounter a change that prevents complete structure reuse.
// During this interval, creation of the file will go unnoticed. We expect this to be
// both rare and low-impact.
for (const missingFilePath of oldProgram.getMissingFilePaths()) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: just use oldProgram.getMissingFilePaths().some(path => host.fileExists(path))

Comment thread src/compiler/sys.ts
const created = !isCurrZero && isPrevZero;
const deleted = isCurrZero && !isPrevZero;

const eventKind = created
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: No need for created and deleted variables, just use eventKind === FileWatcherEventKind.Changed instead of !created && !deleted

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the existence of the constants makes the code more readable, but I agree that the if is better expressed in terms of eventKind.

Comment thread src/compiler/tsc.ts Outdated
reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes));

const missingPaths = compileResult.program.getMissingFilePaths();
missingPaths.forEach((path: Path): void => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Avoid annotating the types of callbacks if not necessary.

Comment thread src/server/server.ts Outdated
return;
}

// removed = deleted ? true : (added ? false : undefined)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment outdated

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, missed one.

amcasey added 7 commits June 29, 2017 10:39
`getTouchingWord` indicates failure by returning the sourceFile node,
rather than `undefined`.
1. Expose missing files from the `Program`.
2. In `tsc --watch` and `tsserver`, add file watchers to missing files.
3. When missing files are created, schedule compilation (tsc) or refresh
the containing projects (tsserver).
Call `this.projectService.host.watchFile`, rather than
`ts.sys.watchFile` so that it gets mocked correctly in the unit tests.
Repair two failing tests.
1. Test `Program.getMissingFilePaths`
2. Test program structure reuse (i.e. that the appearance of a missing
file prevents complete reuse)
Make Program.getMissingFilePaths required

Assume getMissingFilePaths always returns a defined value

Make getMissingFilePaths internal

Replace nullable-bool with enum

Update type to reflect possibility of undefined

Use deepEqual to simplify tests

Make condition const

Don't bother cleaning up map before freeing it

Switch from foreach to for-of to simplify debugging

Use a Map, rather than a FileMap, to track open FileWatchers

Fix compilation errors

Introduce and consume arrayToSet

Fix lint warnings about misplaced braces

Delete incorrect comment

Delete from map during iteration

Eliminate unnecessary type annotations
@amcasey amcasey merged commit ac72803 into microsoft:master Jun 29, 2017
@amcasey amcasey deleted the Vsts434619 branch June 29, 2017 20:41
Comment thread src/server/project.ts
// Missing files that are not yet watched should be added to the map.
for (const missingFilePath of missingFilePaths) {
if (!this.missingFilesMap.has(missingFilePath)) {
const fileWatcher = this.projectService.host.watchFile(missingFilePath, (_filename: string, eventKind: FileWatcherEventKind) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

host.watchFile is actually optional. We could make it non-optional and mark it as a breaking change. See also #16776.

Comment thread src/compiler/core.ts
*
* @param array the array of input elements.
*/
export function arrayToSet<T>(array: T[], makeKey: (value: T) => string): Map<true> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used exactly ONCE. Is it worth extracting like that -- from performance, code clarity or any other perspective?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's generally useful to factor generic code out of a more specific context. That way you don't have to worry about the specifics of how an array is converted to a set, and only have to worry about the actual problem domain.

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants