fix(language-service): update to use CompilerHost from compiler-cli (#13189)

This commit is contained in:
Chuck Jazdzewski
2016-12-02 14:34:16 -08:00
committed by Alex Rickabaugh
parent dfd8140084
commit 3ff6554cbc
11 changed files with 179 additions and 160 deletions

View File

@ -55,12 +55,14 @@ export function getDeclarationDiagnostics(
let directives: Set<StaticSymbol>|undefined = undefined;
for (const declaration of declarations) {
let report = (message: string) => {
results.push(
<Diagnostic>{kind: DiagnosticKind.Error, span: declaration.declarationSpan, message});
const report = (message: string, span?: Span) => {
results.push(<Diagnostic>{
kind: DiagnosticKind.Error,
span: span || declaration.declarationSpan, message
});
};
if (declaration.error) {
report(declaration.error);
for (const error of declaration.errors) {
report(error.message, error.span);
}
if (declaration.metadata) {
if (declaration.metadata.isComponent) {

View File

@ -6,28 +6,16 @@
* found in the LICENSE file at https://angular.io/license
*/
import {StaticReflectorHost, StaticSymbol} from '@angular/compiler';
import {MetadataCollector} from '@angular/tsc-wrapped/src/collector';
import {ModuleMetadata} from '@angular/tsc-wrapped/src/schema';
import * as path from 'path';
import {AngularCompilerOptions, AotCompilerHost, CompilerHost, ModuleResolutionHostAdapter} from '@angular/compiler-cli';
import * as ts from 'typescript';
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const DTS = /\.d\.ts$/;
let serialNumber = 0;
class ReflectorModuleModuleResolutionHost implements ts.ModuleResolutionHost {
private forceExists: string[] = [];
constructor(private host: ts.LanguageServiceHost) {
if (host.directoryExists)
this.directoryExists = directoryName => this.host.directoryExists(directoryName);
}
fileExists(fileName: string): boolean {
return !!this.host.getScriptSnapshot(fileName) || this.forceExists.indexOf(fileName) >= 0;
}
fileExists(fileName: string): boolean { return !!this.host.getScriptSnapshot(fileName); }
readFile(fileName: string): string {
let snapshot = this.host.getScriptSnapshot(fileName);
@ -37,122 +25,19 @@ class ReflectorModuleModuleResolutionHost implements ts.ModuleResolutionHost {
}
directoryExists: (directoryName: string) => boolean;
forceExist(fileName: string): void { this.forceExists.push(fileName); }
}
export class ReflectorHost implements StaticReflectorHost {
private metadataCollector: MetadataCollector;
private moduleResolverHost: ReflectorModuleModuleResolutionHost;
private _typeChecker: ts.TypeChecker;
private metadataCache = new Map<string, MetadataCacheEntry>();
export class ReflectorHost extends CompilerHost {
constructor(
private getProgram: () => ts.Program, private serviceHost: ts.LanguageServiceHost,
private options: ts.CompilerOptions, private basePath: string) {
this.moduleResolverHost = new ReflectorModuleModuleResolutionHost(serviceHost);
this.metadataCollector = new MetadataCollector();
private getProgram: () => ts.Program, serviceHost: ts.LanguageServiceHost,
options: AngularCompilerOptions) {
super(
null, options,
new ModuleResolutionHostAdapter(new ReflectorModuleModuleResolutionHost(serviceHost)));
}
getCanonicalFileName(fileName: string): string { return fileName; }
private get program() { return this.getProgram(); }
public moduleNameToFileName(moduleName: string, containingFile: string) {
if (!containingFile || !containingFile.length) {
if (moduleName.indexOf('.') === 0) {
throw new Error('Resolution of relative paths requires a containing file.');
}
// Any containing file gives the same result for absolute imports
containingFile = this.getCanonicalFileName(path.join(this.basePath, 'index.ts'));
}
moduleName = moduleName.replace(EXT, '');
const resolved =
ts.resolveModuleName(moduleName, containingFile, this.options, this.moduleResolverHost)
.resolvedModule;
return resolved ? resolved.resolvedFileName : null;
};
/**
* We want a moduleId that will appear in import statements in the generated code.
* These need to be in a form that system.js can load, so absolute file paths don't work.
* Relativize the paths by checking candidate prefixes of the absolute path, to see if
* they are resolvable by the moduleResolution strategy from the CompilerHost.
*/
fileNameToModuleName(importedFile: string, containingFile: string) {
// TODO(tbosch): if a file does not yet exist (because we compile it later),
// we still need to create it so that the `resolve` method works!
if (!this.moduleResolverHost.fileExists(importedFile)) {
this.moduleResolverHost.forceExist(importedFile);
}
const parts = importedFile.replace(EXT, '').split(path.sep).filter(p => !!p);
for (let index = parts.length - 1; index >= 0; index--) {
let candidate = parts.slice(index, parts.length).join(path.sep);
if (this.moduleNameToFileName('.' + path.sep + candidate, containingFile) === importedFile) {
return `./${candidate}`;
}
if (this.moduleNameToFileName(candidate, containingFile) === importedFile) {
return candidate;
}
}
throw new Error(
`Unable to find any resolvable import for ${importedFile} relative to ${containingFile}`);
}
private get typeChecker(): ts.TypeChecker {
let result = this._typeChecker;
if (!result) {
result = this._typeChecker = this.program.getTypeChecker();
}
return result;
}
private typeCache = new Map<string, StaticSymbol>();
// TODO(alexeagle): take a statictype
getMetadataFor(filePath: string): ModuleMetadata[] {
if (!this.moduleResolverHost.fileExists(filePath)) {
throw new Error(`No such file '${filePath}'`);
}
if (DTS.test(filePath)) {
const metadataPath = filePath.replace(DTS, '.metadata.json');
if (this.moduleResolverHost.fileExists(metadataPath)) {
return this.readMetadata(metadataPath);
}
}
let sf = this.program.getSourceFile(filePath);
if (!sf) {
throw new Error(`Source file ${filePath} not present in program.`);
}
const entry = this.metadataCache.get(sf.path);
const version = this.serviceHost.getScriptVersion(sf.path);
if (entry && entry.version == version) {
if (!entry.content) return undefined;
return [entry.content];
}
const metadata = this.metadataCollector.getMetadata(sf);
this.metadataCache.set(sf.path, {version, content: metadata});
if (metadata) return [metadata];
}
readMetadata(filePath: string) {
try {
const text = this.moduleResolverHost.readFile(filePath);
const result = JSON.parse(text);
if (!Array.isArray(result)) return [result];
return result;
} catch (e) {
console.error(`Failed to read JSON file ${filePath}`);
throw e;
}
protected get program() { return this.getProgram(); }
protected set program(value: ts.Program) {
// Discard the result set by ancestor constructor
}
}
interface MetadataCacheEntry {
version: string;
content: ModuleMetadata;
}

View File

@ -87,6 +87,25 @@ export interface TemplateSource {
*/
export type TemplateSources = TemplateSource[] /* | undefined */;
/**
* Error information found getting declaration information
*
* A host type; see `LanagueServiceHost`.
*
* @experimental
*/
export interface DeclarationError {
/**
* The span of the error in the declaration's module.
*/
readonly span: Span;
/**
* The message to display describing the error.
*/
readonly message: string;
}
/**
* Information about the component declarations.
*
@ -117,11 +136,10 @@ export interface Declaration {
*/
readonly metadata?: CompileDirectiveMetadata;
/**
* Error reported trying to get the metadata.
*/
readonly error?: string;
readonly errors: DeclarationError[];
}
/**

View File

@ -27,8 +27,7 @@ import * as ts from 'typescript';
import {createLanguageService} from './language_service';
import {ReflectorHost} from './reflector_host';
import {BuiltinType, CompletionKind, Declaration, Declarations, Definition, LanguageService, LanguageServiceHost, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable, TemplateSource, TemplateSources} from './types';
import {BuiltinType, CompletionKind, Declaration, DeclarationError, Declarations, Definition, LanguageService, LanguageServiceHost, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable, TemplateSource, TemplateSources} from './types';
/**
@ -89,6 +88,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
private service: LanguageService;
private fileToComponent: Map<string, StaticSymbol>;
private templateReferences: string[];
private collectedErrors: Map<string, any[]>;
constructor(
typescript: typeof ts, private host: ts.LanguageServiceHost,
@ -158,6 +158,10 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
getAnalyzedModules(): NgAnalyzedModules {
this.validate();
return this.ensureAnalyzedModules();
}
private ensureAnalyzedModules(): NgAnalyzedModules {
let analyzedModules = this.analyzedModules;
if (!analyzedModules) {
const programSymbols = extractProgramSymbols(
@ -221,9 +225,14 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
}
updateAnalyzedModules() {
this.validate();
if (this.modulesOutOfDate) {
this.analyzedModules = null;
this.getAnalyzedModules();
this._reflector = null;
this.templateReferences = null;
this.fileToComponent = null;
this.ensureAnalyzedModules();
this.modulesOutOfDate = false;
}
}
@ -249,10 +258,8 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
this._checker = null;
this._typeCache = [];
this._resolver = null;
this._reflector = null;
this.collectedErrors = null;
this.modulesOutOfDate = true;
this.templateReferences = null;
this.fileToComponent = null;
}
private ensureTemplateMap() {
@ -361,19 +368,34 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
throw new Error('Internal error: no context could be determined');
}
const tsConfigPath = findTsConfig(source.path);
const tsConfigPath = findTsConfig(source.fileName);
const basePath = path.dirname(tsConfigPath || this.context);
result = this._reflectorHost = new ReflectorHost(
() => this.tsService.getProgram(), this.host, this.host.getCompilationSettings(),
basePath);
() => this.tsService.getProgram(), this.host, {basePath, genDir: basePath});
}
return result;
}
private collectError(error: any, filePath: string) {
let errorMap = this.collectedErrors;
if (!errorMap) {
errorMap = this.collectedErrors = new Map();
}
let errors = errorMap.get(filePath);
if (!errors) {
errors = [];
this.collectedErrors.set(filePath, errors);
}
errors.push(error);
}
private get reflector(): StaticReflector {
let result = this._reflector;
if (!result) {
result = this._reflector = new StaticReflector(this.reflectorHost, this._staticSymbolCache);
result = this._reflector = new StaticReflector(
this.reflectorHost, this._staticSymbolCache, [], [],
(e, filePath) => this.collectError(e, filePath));
}
return result;
}
@ -440,6 +462,14 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return [declaration, callTarget];
}
private getCollectedErrors(defaultSpan: Span, sourceFile: ts.SourceFile): DeclarationError[] {
const errors = (this.collectedErrors && this.collectedErrors.get(sourceFile.fileName));
return (errors && errors.map((e: any) => {
return {message: e.message, span: spanAt(sourceFile, e.line, e.column) || defaultSpan};
})) ||
[];
}
private getDeclarationFromNode(sourceFile: ts.SourceFile, node: ts.Node): Declaration|undefined {
if (node.kind == ts.SyntaxKind.ClassDeclaration && node.decorators &&
(node as ts.ClassDeclaration).name) {
@ -457,14 +487,22 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
if (this.resolver.isDirective(staticSymbol as any)) {
const {metadata} =
this.resolver.getNonNormalizedDirectiveMetadata(staticSymbol as any);
return {type: staticSymbol, declarationSpan: spanOf(target), metadata};
const declarationSpan = spanOf(target);
return {
type: staticSymbol,
declarationSpan,
metadata,
errors: this.getCollectedErrors(declarationSpan, sourceFile)
};
}
} catch (e) {
if (e.message) {
this.collectError(e, sourceFile.fileName);
const declarationSpan = spanOf(target);
return {
type: staticSymbol,
declarationSpan: spanAt(sourceFile, e.line, e.column) || spanOf(target),
error: e.message
declarationSpan,
errors: this.getCollectedErrors(declarationSpan, sourceFile)
};
}
}