fix(language-service): getSourceFile() should only be called on TS files (#31920)

PR Close #31920
This commit is contained in:
Keen Yee Liau 2019-07-30 15:51:47 -07:00 committed by Alex Rickabaugh
parent 9e9179e915
commit e8b8f6d09b
2 changed files with 70 additions and 51 deletions

View File

@ -117,22 +117,29 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return this.templateReferences || [];
}
/**
* Get the Angular template in the file, if any. If TS file is provided then
* return the inline template, otherwise return the external template.
* @param fileName Either TS or HTML file
* @param position Only used if file is TS
*/
getTemplateAt(fileName: string, position: number): TemplateSource|undefined {
let sourceFile = this.getSourceFile(fileName);
if (sourceFile) {
this.context = sourceFile.fileName;
let node = this.findNode(sourceFile, position);
if (node) {
return this.getSourceFromNode(
fileName, this.host.getScriptVersion(sourceFile.fileName), node);
if (fileName.endsWith('.ts')) {
const sourceFile = this.getSourceFile(fileName);
if (sourceFile) {
this.context = sourceFile.fileName;
const node = this.findNode(sourceFile, position);
if (node) {
return this.getSourceFromNode(
fileName, this.host.getScriptVersion(sourceFile.fileName), node);
}
}
} else {
this.ensureTemplateMap();
// TODO: Cannocalize the file?
const componentType = this.fileToComponent.get(fileName);
if (componentType) {
const componentSymbol = this.fileToComponent.get(fileName);
if (componentSymbol) {
return this.getSourceFromType(
fileName, this.host.getScriptVersion(fileName), componentType);
fileName, this.host.getScriptVersion(fileName), componentSymbol);
}
}
return undefined;
@ -164,14 +171,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
}
getTemplates(fileName: string): TemplateSources {
this.ensureTemplateMap();
const componentType = this.fileToComponent.get(fileName);
if (componentType) {
const templateSource = this.getTemplateAt(fileName, 0);
if (templateSource) {
return [templateSource];
}
} else {
if (fileName.endsWith('.ts')) {
let version = this.host.getScriptVersion(fileName);
let result: TemplateSource[] = [];
@ -191,10 +191,22 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
ts.forEachChild(sourceFile, visit);
}
return result.length ? result : undefined;
} else {
this.ensureTemplateMap();
const componentSymbol = this.fileToComponent.get(fileName);
if (componentSymbol) {
const templateSource = this.getTemplateAt(fileName, 0);
if (templateSource) {
return [templateSource];
}
}
}
}
getDeclarations(fileName: string): Declarations {
if (!fileName.endsWith('.ts')) {
return [];
}
const result: Declarations = [];
const sourceFile = this.getSourceFile(fileName);
if (sourceFile) {
@ -212,6 +224,9 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
}
getSourceFile(fileName: string): ts.SourceFile|undefined {
if (!fileName.endsWith('.ts')) {
throw new Error(`Non-TS source file requested: ${fileName}`);
}
return this.tsService.getProgram() !.getSourceFile(fileName);
}
@ -383,7 +398,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
// The host's getCurrentDirectory() is not reliable as it is always "" in
// tsserver. We don't need the exact base directory, just one that contains
// a source file.
const source = this.tsService.getProgram() !.getSourceFile(this.context);
const source = this.getSourceFile(this.context);
if (!source) {
throw new Error('Internal error: no context could be determined');
}

View File

@ -15,51 +15,55 @@ import {toh} from './test_data';
import {MockTypescriptHost} from './test_utils';
describe('completions', () => {
let host: ts.LanguageServiceHost;
let service: ts.LanguageService;
let ngHost: TypeScriptServiceHost;
beforeEach(() => {
host = new MockTypescriptHost(['/app/main.ts'], toh);
service = ts.createLanguageService(host);
describe('TypeScriptServiceHost', () => {
it('should be able to create a typescript host', () => {
const tsLSHost = new MockTypescriptHost(['/app/main.ts'], toh);
const tsLS = ts.createLanguageService(tsLSHost);
expect(() => new TypeScriptServiceHost(tsLSHost, tsLS)).not.toThrow();
});
it('should be able to create a typescript host',
() => { expect(() => new TypeScriptServiceHost(host, service)).not.toThrow(); });
it('should be able to analyze modules', () => {
const tsLSHost = new MockTypescriptHost(['/app/main.ts'], toh);
const tsLS = ts.createLanguageService(tsLSHost);
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
expect(ngLSHost.getAnalyzedModules()).toBeDefined();
});
beforeEach(() => { ngHost = new TypeScriptServiceHost(host, service); });
it('should be able to analyze modules',
() => { expect(ngHost.getAnalyzedModules()).toBeDefined(); });
it('should be able to analyze modules in without a tsconfig.json file', () => {
host = new MockTypescriptHost(['foo.ts'], toh);
service = ts.createLanguageService(host);
ngHost = new TypeScriptServiceHost(host, service);
expect(ngHost.getAnalyzedModules()).toBeDefined();
it('should be able to analyze modules without a tsconfig.json file', () => {
const tsLSHost = new MockTypescriptHost(['foo.ts'], toh);
const tsLS = ts.createLanguageService(tsLSHost);
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
expect(ngLSHost.getAnalyzedModules()).toBeDefined();
});
it('should not throw if there is no script names', () => {
host = new MockTypescriptHost([], toh);
service = ts.createLanguageService(host);
ngHost = new TypeScriptServiceHost(host, service);
expect(() => ngHost.getAnalyzedModules()).not.toThrow();
const tsLSHost = new MockTypescriptHost([], toh);
const tsLS = ts.createLanguageService(tsLSHost);
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
expect(() => ngLSHost.getAnalyzedModules()).not.toThrow();
});
it('should clear the caches if program changes', () => {
// First create a TypescriptHost with empty script names
host = new MockTypescriptHost([], toh);
service = ts.createLanguageService(host);
ngHost = new TypeScriptServiceHost(host, service);
expect(ngHost.getAnalyzedModules().ngModules).toEqual([]);
const tsLSHost = new MockTypescriptHost([], toh);
const tsLS = ts.createLanguageService(tsLSHost);
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
expect(ngLSHost.getAnalyzedModules().ngModules).toEqual([]);
// Now add a script, this would change the program
const fileName = '/app/main.ts';
const content = (host as MockTypescriptHost).getFileContent(fileName) !;
(host as MockTypescriptHost).addScript(fileName, content);
const content = (tsLSHost as MockTypescriptHost).getFileContent(fileName) !;
(tsLSHost as MockTypescriptHost).addScript(fileName, content);
// If the caches are not cleared, we would get back an empty array.
// But if the caches are cleared then the analyzed modules will be non-empty.
expect(ngHost.getAnalyzedModules().ngModules.length).not.toEqual(0);
expect(ngLSHost.getAnalyzedModules().ngModules.length).not.toEqual(0);
});
it('should throw if getSourceFile is called on non-TS file', () => {
const tsLSHost = new MockTypescriptHost([], toh);
const tsLS = ts.createLanguageService(tsLSHost);
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
expect(() => {
ngLSHost.getSourceFile('/src/test.ng');
}).toThrowError('Non-TS source file requested: /src/test.ng');
});
});