Watch generated file if it doesnt exist when trying to translate it to to source generated position

This commit is contained in:
Sheetal Nandi 2019-06-25 12:15:04 -07:00
parent 903527e757
commit 3e49556a88
5 changed files with 201 additions and 58 deletions

View file

@ -2234,7 +2234,13 @@ namespace ts.server {
getDocumentPositionMapper(project: Project, generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined {
// Since declaration info and map file watches arent updating project's directory structure host (which can cache file structure) use host
const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(generatedFileName, project.currentDirectory, this.host);
if (!declarationInfo) return undefined;
if (!declarationInfo) {
if (sourceFileName) {
// Project contains source file and it generates the generated file name
project.addGeneratedFileWatch(generatedFileName, sourceFileName);
}
return undefined;
}
// Try to get from cache
declarationInfo.getSnapshot(); // Ensure synchronized

View file

@ -109,12 +109,22 @@ namespace ts.server {
return value instanceof ScriptInfo;
}
interface GeneratedFileWatcher {
generatedFilePath: Path;
watcher: FileWatcher;
}
type GeneratedFileWatcherMap = GeneratedFileWatcher | Map<GeneratedFileWatcher>;
function isGeneratedFileWatcher(watch: GeneratedFileWatcherMap): watch is GeneratedFileWatcher {
return (watch as GeneratedFileWatcher).generatedFilePath !== undefined;
}
export abstract class Project implements LanguageServiceHost, ModuleResolutionHost {
private rootFiles: ScriptInfo[] = [];
private rootFilesMap: Map<ProjectRoot> = createMap<ProjectRoot>();
private program: Program | undefined;
private externalFiles: SortedReadonlyArray<string> | undefined;
private missingFilesMap: Map<FileWatcher> | undefined;
private generatedFilesMap: GeneratedFileWatcherMap | undefined;
private plugins: PluginModuleWithName[] = [];
/*@internal*/
@ -568,6 +578,7 @@ namespace ts.server {
this.lastFileExceededProgramSize = lastFileExceededProgramSize;
this.builderState = undefined;
this.resolutionCache.closeTypeRootsWatch();
this.clearGeneratedFileWatch();
this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false);
}
@ -649,6 +660,7 @@ namespace ts.server {
clearMap(this.missingFilesMap, closeFileWatcher);
this.missingFilesMap = undefined!;
}
this.clearGeneratedFileWatch();
// signal language service to release source files acquired from document registry
this.languageService.dispose();
@ -942,6 +954,39 @@ namespace ts.server {
missingFilePath => this.addMissingFileWatcher(missingFilePath)
);
if (this.generatedFilesMap) {
const outPath = this.compilerOptions.outFile && this.compilerOptions.out;
if (isGeneratedFileWatcher(this.generatedFilesMap)) {
// --out
if (!outPath || !this.isValidGeneratedFileWatcher(
removeFileExtension(outPath) + Extension.Dts,
this.generatedFilesMap,
)) {
this.clearGeneratedFileWatch();
}
}
else {
// MultiFile
if (outPath) {
this.clearGeneratedFileWatch();
}
else {
this.generatedFilesMap.forEach((watcher, source) => {
const sourceFile = this.program!.getSourceFileByPath(source as Path);
if (!sourceFile ||
sourceFile.resolvedPath !== source ||
!this.isValidGeneratedFileWatcher(
getDeclarationEmitOutputFilePathWorker(sourceFile.fileName, this.compilerOptions, this.currentDirectory, this.program!.getCommonSourceDirectory(), this.getCanonicalFileName),
watcher
)) {
closeFileWatcherOf(watcher);
(this.generatedFilesMap as Map<GeneratedFileWatcher>).delete(source);
}
});
}
}
}
// Watch the type locations that would be added to program as part of automatic type resolutions
if (this.languageServiceEnabled) {
this.resolutionCache.updateTypeRootsWatch();
@ -1006,6 +1051,61 @@ namespace ts.server {
return !!this.missingFilesMap && this.missingFilesMap.has(path);
}
/* @internal */
addGeneratedFileWatch(generatedFile: string, sourceFile: string) {
if (this.compilerOptions.outFile || this.compilerOptions.out) {
// Single watcher
if (!this.generatedFilesMap) {
this.generatedFilesMap = this.createGeneratedFileWatcher(generatedFile);
}
}
else {
// Map
const path = this.toPath(sourceFile);
if (this.generatedFilesMap) {
if (isGeneratedFileWatcher(this.generatedFilesMap)) {
Debug.fail(`${this.projectName} Expected not to have --out watcher for generated file with options: ${JSON.stringify(this.compilerOptions)}`);
return;
}
if (this.generatedFilesMap.has(path)) return;
}
else {
this.generatedFilesMap = createMap();
}
this.generatedFilesMap.set(path, this.createGeneratedFileWatcher(generatedFile));
}
}
private createGeneratedFileWatcher(generatedFile: string): GeneratedFileWatcher {
return {
generatedFilePath: this.toPath(generatedFile),
watcher: this.projectService.watchFactory.watchFile(
this.projectService.host,
generatedFile,
() => this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this),
PollingInterval.High,
WatchType.MissingGeneratedFile,
this
)
};
}
private isValidGeneratedFileWatcher(generateFile: string, watcher: GeneratedFileWatcher) {
return this.toPath(generateFile) === watcher.generatedFilePath;
}
private clearGeneratedFileWatch() {
if (this.generatedFilesMap) {
if (isGeneratedFileWatcher(this.generatedFilesMap)) {
closeFileWatcherOf(this.generatedFilesMap);
}
else {
clearMap(this.generatedFilesMap, closeFileWatcherOf);
}
this.generatedFilesMap = undefined;
}
}
getScriptInfoForNormalizedPath(fileName: NormalizedPath): ScriptInfo | undefined {
const scriptInfo = this.projectService.getScriptInfoForPath(this.toPath(fileName));
if (scriptInfo && !scriptInfo.isAttached(this)) {

View file

@ -227,5 +227,6 @@ namespace ts {
NodeModulesForClosedScriptInfo = "node_modules for closed script infos in them",
MissingSourceMapFile = "Missing source map file",
NoopConfigFileForInferredRoot = "Noop Config file for the inferred project root",
MissingGeneratedFile = "Missing generated file"
}
}

View file

@ -94,6 +94,7 @@ namespace ts.projectSystem {
describe("with main and depedency project", () => {
const projectLocation = "/user/username/projects/myproject";
const dependecyLocation = `${projectLocation}/dependency`;
const dependecyDeclsLocation = `${projectLocation}/decls`;
const mainLocation = `${projectLocation}/main`;
const dependencyTs: File = {
path: `${dependecyLocation}/FnS.ts`,
@ -106,7 +107,7 @@ export function fn5() { }
};
const dependencyConfig: File = {
path: `${dependecyLocation}/tsconfig.json`,
content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true } })
content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true, declarationDir: "../decls" } })
};
const mainTs: File = {
@ -117,7 +118,7 @@ export function fn5() { }
fn3,
fn4,
fn5
} from '../dependency/fns'
} from '../decls/fns'
fn1();
fn2();
@ -142,9 +143,9 @@ fn5();
path: `${projectLocation}/random/tsconfig.json`,
content: "{}"
};
const dtsLocation = `${dependecyLocation}/FnS.d.ts`;
const dtsLocation = `${dependecyDeclsLocation}/FnS.d.ts`;
const dtsPath = dtsLocation.toLowerCase() as Path;
const dtsMapLocation = `${dtsLocation}.map`;
const dtsMapLocation = `${dependecyDeclsLocation}/FnS.d.ts.map`;
const dtsMapPath = dtsMapLocation.toLowerCase() as Path;
const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile, randomFile, randomConfig];
@ -217,7 +218,7 @@ fn5();
start: { line: fn + 1, offset: 5 },
end: { line: fn + 1, offset: 8 },
contextStart: { line: 1, offset: 1 },
contextEnd: { line: 7, offset: 27 }
contextEnd: { line: 7, offset: 22 }
};
}
function usageSpan(fn: number): protocol.TextSpan {
@ -287,19 +288,25 @@ fn5();
function verifyDocumentPositionMapperUpdates(
mainScenario: string,
verifier: ReadonlyArray<DocumentPositionMapperVerifier>,
closedInfos: ReadonlyArray<string>) {
closedInfos: ReadonlyArray<string>,
withRefs: boolean) {
const openFiles = verifier.map(v => v.openFile);
const expectedProjectActualFiles = verifier.map(v => v.expectedProjectActualFiles);
const actionGetters = verifier.map(v => v.actionGetter);
const openFileLastLines = verifier.map(v => v.openFileLastLine);
const configFiles = openFiles.map(openFile => `${getDirectoryPath(openFile.path)}/tsconfig.json`);
const openInfos = openFiles.map(f => f.path);
// When usage and dependency are used, dependency config is part of closedInfo so ignore
const otherWatchedFiles = verifier.length > 1 ? [configFiles[0]] : configFiles;
const otherWatchedFiles = withRefs && verifier.length > 1 ? [configFiles[0]] : configFiles;
function openTsFile(onHostCreate?: (host: TestServerHost) => void) {
const host = createHost(files, [mainConfig.path]);
if (!withRefs) {
// Erase project reference
host.writeFile(mainConfig.path, JSON.stringify({
compilerOptions: { composite: true, declarationMap: true }
}));
}
if (onHostCreate) {
onHostCreate(host);
}
@ -336,7 +343,7 @@ fn5();
);
}
function verifyInfosWhenNoDtsFile(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) {
function verifyInfosWhenNoDtsFile(session: TestSession, host: TestServerHost, watchDts: boolean, dependencyTsAndMapOk?: true) {
const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined);
const dtsClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsPath ? f : undefined);
verifyInfosWithRandom(
@ -344,8 +351,7 @@ fn5();
host,
openInfos,
closedInfos.filter(f => (dependencyTsAndMapOk || f !== dtsMapClosedInfo) && f !== dtsClosedInfo && (dependencyTsAndMapOk || f !== dependencyTs.path)),
// When project actual file contains dts, it needs to be watched
dtsClosedInfo && expectedProjectActualFiles.some(expectedProjectActualFiles => expectedProjectActualFiles.some(f => f.toLowerCase() === dtsPath)) ?
dtsClosedInfo && watchDts ?
otherWatchedFiles.concat(dtsClosedInfo) :
otherWatchedFiles
);
@ -361,22 +367,22 @@ fn5();
}
}
function action(actionGetter: SessionActionGetter, fn: number, session: TestSession) {
const { reqName, request, expectedResponse, expectedResponseNoMap, expectedResponseNoDts } = actionGetter(fn);
function action(verifier: DocumentPositionMapperVerifier, fn: number, session: TestSession) {
const { reqName, request, expectedResponse, expectedResponseNoMap, expectedResponseNoDts } = verifier.actionGetter(fn);
const { response } = session.executeCommandSeq(request);
return { reqName, response, expectedResponse, expectedResponseNoMap, expectedResponseNoDts };
return { reqName, response, expectedResponse, expectedResponseNoMap, expectedResponseNoDts, verifier };
}
function firstAction(session: TestSession) {
actionGetters.forEach(actionGetter => action(actionGetter, 1, session));
verifier.forEach(v => action(v, 1, session));
}
function verifyAllFnActionWorker(session: TestSession, verifyAction: (result: ReturnType<typeof action>, dtsInfo: server.ScriptInfo | undefined, isFirst: boolean) => void, dtsAbsent?: true) {
// action
let isFirst = true;
for (const actionGetter of actionGetters) {
for (const v of verifier) {
for (let fn = 1; fn <= 5; fn++) {
const result = action(actionGetter, fn, session);
const result = action(v, fn, session);
const dtsInfo = session.getProjectService().filenameToScriptInfo.get(dtsPath);
if (dtsAbsent) {
assert.isUndefined(dtsInfo);
@ -449,9 +455,17 @@ fn5();
dependencyTsAndMapOk?: true
) {
// action
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoDts }) => {
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoDts, verifier }) => {
assert.deepEqual(response, expectedResponseNoDts || expectedResponse, `Failed on ${reqName}`);
verifyInfosWhenNoDtsFile(session, host, dependencyTsAndMapOk);
verifyInfosWhenNoDtsFile(
session,
host,
// Even when project actual file contains dts, its not watched because the dts is in another folder and module resolution just fails
// instead of succeeding to source file and then mapping using project reference (When using usage location)
// But watched if sourcemapper is in source project since we need to keep track of dts to update the source mapper for any potential usages
verifier.expectedProjectActualFiles.every(f => f.toLowerCase() !== dtsPath),
dependencyTsAndMapOk,
);
}, /*dtsAbsent*/ true);
}
@ -535,7 +549,11 @@ fn5();
// Collecting at this point retains dependency.d.ts and map watcher
closeFilesForSession([randomFile], session);
openFilesForSession([randomFile], session);
verifyInfosWhenNoDtsFile(session, host);
verifyInfosWhenNoDtsFile(
session,
host,
!!forEach(verifier, v => v.expectedProjectActualFiles.every(f => f.toLowerCase() !== dtsPath))
);
// Closing open file, removes dependencies too
closeFilesForSession([...openFiles, randomFile], session);
@ -616,7 +634,7 @@ fn5();
"when dependency file's map changes",
host => host.writeFile(
dtsMapLocation,
`{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}`
`{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["../dependency/FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}`
),
/*afterActionDocumentPositionMapperNotEquals*/ true
);
@ -635,44 +653,58 @@ fn5();
);
}
const usageVerifier: DocumentPositionMapperVerifier = {
openFile: mainTs,
expectedProjectActualFiles: [mainTs.path, libFile.path, mainConfig.path, dtsPath],
actionGetter: gotoDefintinionFromMainTs,
openFileLastLine: 14
};
describe("from project that uses dependency", () => {
const closedInfos = [dependencyTs.path, dependencyConfig.path, libFile.path, dtsPath, dtsMapLocation];
verifyDocumentPositionMapperUpdates(
"can go to definition correctly",
[usageVerifier],
closedInfos
);
});
function verifyScenarios(withRefs: boolean) {
describe(withRefs ? "when main tsconfig has project reference" : "when main tsconfig doesnt have project reference", () => {
const usageVerifier: DocumentPositionMapperVerifier = {
openFile: mainTs,
expectedProjectActualFiles: [mainTs.path, libFile.path, mainConfig.path, dtsPath],
actionGetter: gotoDefintinionFromMainTs,
openFileLastLine: 14
};
describe("from project that uses dependency", () => {
const closedInfos = withRefs ?
[dependencyTs.path, dependencyConfig.path, libFile.path, dtsPath, dtsMapLocation] :
[dependencyTs.path, libFile.path, dtsPath, dtsMapLocation];
verifyDocumentPositionMapperUpdates(
"can go to definition correctly",
[usageVerifier],
closedInfos,
withRefs
);
});
const definingVerifier: DocumentPositionMapperVerifier = {
openFile: dependencyTs,
expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path],
actionGetter: renameFromDependencyTs,
openFileLastLine: 6
};
describe("from defining project", () => {
const closedInfos = [libFile.path, dtsLocation, dtsMapLocation];
verifyDocumentPositionMapperUpdates(
"rename locations from dependency",
[definingVerifier],
closedInfos
);
});
const definingVerifier: DocumentPositionMapperVerifier = {
openFile: dependencyTs,
expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path],
actionGetter: renameFromDependencyTs,
openFileLastLine: 6,
};
describe("from defining project", () => {
const closedInfos = [libFile.path, dtsLocation, dtsMapLocation];
verifyDocumentPositionMapperUpdates(
"rename locations from dependency",
[definingVerifier],
closedInfos,
withRefs
);
});
describe("when opening depedency and usage project", () => {
const closedInfos = [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path];
verifyDocumentPositionMapperUpdates(
"goto Definition in usage and rename locations from defining project",
[usageVerifier, { ...definingVerifier, actionGetter: renameFromDependencyTsWithBothProjectsOpen }],
closedInfos
);
});
describe("when opening depedency and usage project", () => {
const closedInfos = withRefs ?
[libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path] :
[libFile.path, dtsPath, dtsMapLocation];
verifyDocumentPositionMapperUpdates(
"goto Definition in usage and rename locations from defining project",
[usageVerifier, { ...definingVerifier, actionGetter: renameFromDependencyTsWithBothProjectsOpen }],
closedInfos,
withRefs
);
});
});
}
verifyScenarios(/*withRefs*/ false);
verifyScenarios(/*withRefs*/ true);
});
});
}

View file

@ -8397,6 +8397,7 @@ declare namespace ts.server {
private program;
private externalFiles;
private missingFilesMap;
private generatedFilesMap;
private plugins;
private lastFileExceededProgramSize;
protected languageService: LanguageService;
@ -8509,6 +8510,9 @@ declare namespace ts.server {
private detachScriptInfoFromProject;
private addMissingFileWatcher;
private isWatchedMissingFile;
private createGeneratedFileWatcher;
private isValidGeneratedFileWatcher;
private clearGeneratedFileWatch;
getScriptInfoForNormalizedPath(fileName: NormalizedPath): ScriptInfo | undefined;
getScriptInfo(uncheckedFileName: string): ScriptInfo | undefined;
filesToString(writeProjectFileNames: boolean): string;