Symbols missing from nodes when SourceFile requested via LanguageService, but not via Program
I got tripped up when using the compiler API, and I wanted to see if it was just me being confused or an actual bug. :) It seems like SourceFiles that are returned from LanguageService.getSourceFile() don't include symbols, whereas those loaded via Program.getSourceFile() do.
If I request a file via:
languageService.getSourceFile("example.ts");
...it will be returned without symbols, so if I try calling any of TypeChecker's methods on it, it will either throw errors or returned undefined. If instead I request files via:
languageService.getProgram().getSourceFile("example.ts");
..the returned file has the symbols and works fine with TypeChecker.
I understand that mixing these two APIs is probably not a normal use case. I'm working on a tool that alters the source of a program as it's being compiled, so having access to LanguageService is really helpful, but I also need the TypeChecker.
(Also, if this is just a problem with my implementation of LanguageServiceHost, please let me know!)
TypeScript Version: Behavior appears on 1.8.7, 1.8.9, and 1.9.0-dev.20160318
Test Case
test.ts
import * as ts from 'typescript'; import * as fs from 'fs'; interface ScriptInfo { text: string version: number } export class LanguageServiceHost implements ts.LanguageServiceHost { files: { [name: string]: ScriptInfo } = {}; constructor(protected rootFileNames: string[], protected options: ts.CompilerOptions) { for (const fileName of rootFileNames) { this.getScriptInfo(fileName); } } getCompilationSettings() { return this.options; } getScriptFileNames() { return this.rootFileNames; } getScriptVersion(fileName: string) { let info = this.getScriptInfo(fileName); return info.version.toString(); } getScriptSnapshot(fileName: string) { let info = this.getScriptInfo(fileName); return ts.ScriptSnapshot.fromString(info.text); } getCurrentDirectory() { return process.cwd(); } getDefaultLibFileName(options: ts.CompilerOptions) { return ts.getDefaultLibFilePath(options); } getScriptInfo(fileName: string): ScriptInfo { let info = this.files[fileName]; if (!info) { this.files[fileName] = info = { text: fs.readFileSync(fileName).toString(), version: 1 }; } return info; } } const languageServiceHost = new LanguageServiceHost(['case.ts'], { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS }); const languageService = ts.createLanguageService(languageServiceHost, ts.createDocumentRegistry()); const program = languageService.getProgram(); const checker = program.getTypeChecker(); function listClassMethods(file: ts.SourceFile) { let visit = (node: ts.Node) => { if (node.kind == ts.SyntaxKind.ClassDeclaration) { const decl = <ts.ClassDeclaration> node; for (const member of decl.members) { if (member.kind == ts.SyntaxKind.MethodDeclaration) { const method = <ts.MethodDeclaration> member; const symbol = checker.getSymbolAtLocation(method.name); if (symbol) { console.log(symbol.getName()); } else { console.log("Couldn't resolve symbol") } } } } ts.forEachChild(node, visit); } ts.forEachChild(file, visit); } listClassMethods(languageService.getSourceFile('case.ts')); listClassMethods(program.getSourceFile('case.ts'));
case.ts
interface IExample { getName: () => string } class A implements IExample { getName() { return 'A'; } } class B implements IExample { getName() { return 'B'; } }
Expected behavior:
$ node test.js
getName
getName
getName
getName
Actual behavior:
$ node test.js
Couldn't resolve symbol
Couldn't resolve symbol
getName
getName
Thanks for your help, and for the great work on TypeScript and the compiler API!