From 3ff6554cbc89a9ee42e33c9af8e70e0d19074417 Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Fri, 2 Dec 2016 14:34:16 -0800 Subject: [PATCH] fix(language-service): update to use `CompilerHost` from compiler-cli (#13189) --- build.sh | 5 + .../@angular/compiler-cli/tsconfig-2015.json | 34 +++++ .../compiler/src/aot/static_reflector.ts | 60 ++++++-- .../language-service/rollup.config.js | 2 +- .../language-service/src/diagnostics.ts | 12 +- .../language-service/src/reflector_host.ts | 137 ++---------------- .../@angular/language-service/src/types.ts | 22 ++- .../language-service/src/typescript_host.ts | 64 ++++++-- .../language-service/test/completions_spec.ts | 1 + .../language-service/test/diagnostics_spec.ts | 1 + .../language-service/tsconfig-build.json | 1 + 11 files changed, 179 insertions(+), 160 deletions(-) create mode 100644 modules/@angular/compiler-cli/tsconfig-2015.json diff --git a/build.sh b/build.sh index 05dfbd24da..0888aa9c8d 100755 --- a/build.sh +++ b/build.sh @@ -145,6 +145,11 @@ do $TSC -p ${SRCDIR}/tsconfig-testing.json fi + if [[ -e ${SRCDIR}/tsconfig-2015.json ]]; then + echo "====== COMPILING ESM: ${TSC} -p ${SRCDIR}/tsconfig-2015.json" + ${TSC} -p ${SRCDIR}/tsconfig-2015.json + fi + echo "====== TSC 1.8 d.ts compat for ${DESTDIR} =====" # safely strips 'readonly' specifier from d.ts files to make them compatible with tsc 1.8 if [ "$(uname)" == "Darwin" ]; then diff --git a/modules/@angular/compiler-cli/tsconfig-2015.json b/modules/@angular/compiler-cli/tsconfig-2015.json new file mode 100644 index 0000000000..d55b4b722a --- /dev/null +++ b/modules/@angular/compiler-cli/tsconfig-2015.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "declaration": true, + "experimentalDecorators": true, + "noImplicitAny": true, + "module": "es2015", + "moduleResolution": "node", + "outDir": "../../../dist/packages-dist/esm/compiler-cli", + "paths": { + "@angular/core": ["../../../dist/packages-dist/core"], + "@angular/common": ["../../../dist/packages-dist/common"], + "@angular/compiler": ["../../../dist/packages-dist/compiler"], + "@angular/platform-server": ["../../../dist/packages-dist/platform-server"], + "@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"], + "@angular/tsc-wrapped": ["../../../dist/tools/@angular/tsc-wrapped"] + }, + "rootDir": ".", + "sourceMap": true, + "inlineSources": true, + "target": "es5", + "lib": ["es6", "dom"], + "skipLibCheck": true + }, + "exclude": ["integrationtest"], + "files": [ + "index.ts", + "src/main.ts", + "src/extract_i18n.ts", + "../../../node_modules/@types/node/index.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/zone.js/dist/zone.js.d.ts" + ] +} diff --git a/modules/@angular/compiler/src/aot/static_reflector.ts b/modules/@angular/compiler/src/aot/static_reflector.ts index 051f20f931..262b85ac77 100644 --- a/modules/@angular/compiler/src/aot/static_reflector.ts +++ b/modules/@angular/compiler/src/aot/static_reflector.ts @@ -81,7 +81,8 @@ export class StaticReflector implements ReflectorReader { private host: StaticReflectorHost, private staticSymbolCache: StaticSymbolCache = new StaticSymbolCache(), knownMetadataClasses: {name: string, filePath: string, ctor: any}[] = [], - knownMetadataFunctions: {name: string, filePath: string, fn: any}[] = []) { + knownMetadataFunctions: {name: string, filePath: string, fn: any}[] = [], + private errorRecorder?: (error: any, fileName: string) => void) { this.initializeConversionMap(); knownMetadataClasses.forEach( (kc) => this._registerDecoratorOrConstructor( @@ -155,7 +156,10 @@ export class StaticReflector implements ReflectorReader { public parameters(type: StaticSymbol): any[] { if (!(type instanceof StaticSymbol)) { - throw new Error(`parameters received ${JSON.stringify(type)} which is not a StaticSymbol`); + this.reportError( + new Error(`parameters received ${JSON.stringify(type)} which is not a StaticSymbol`), + type); + return []; } try { let parameters = this.parameterCache.get(type); @@ -219,8 +223,10 @@ export class StaticReflector implements ReflectorReader { hasLifecycleHook(type: any, lcProperty: string): boolean { if (!(type instanceof StaticSymbol)) { - throw new Error( - `hasLifecycleHook received ${JSON.stringify(type)} which is not a StaticSymbol`); + this.reportError( + new Error( + `hasLifecycleHook received ${JSON.stringify(type)} which is not a StaticSymbol`), + type); } try { return !!this._methodNames(type)[lcProperty]; @@ -301,11 +307,21 @@ export class StaticReflector implements ReflectorReader { return this.staticSymbolCache.get(declarationFile, name, members); } + private reportError(error: Error, context: StaticSymbol, path?: string) { + if (this.errorRecorder) { + this.errorRecorder(error, (context && context.filePath) || path); + } else { + throw error; + } + } + private resolveExportedSymbol(filePath: string, symbolName: string): StaticSymbol { const resolveModule = (moduleName: string): string => { const resolvedModulePath = this.host.moduleNameToFileName(moduleName, filePath); if (!resolvedModulePath) { - throw new Error(`Could not resolve module '${moduleName}' relative to file ${filePath}`); + this.reportError( + new Error(`Could not resolve module '${moduleName}' relative to file ${filePath}`), + null, filePath); } return resolvedModulePath; }; @@ -338,7 +354,12 @@ export class StaticReflector implements ReflectorReader { if (typeof exportSymbol !== 'string') { symName = exportSymbol.name; } - staticSymbol = this.resolveExportedSymbol(resolveModule(moduleExport.from), symName); + const resolvedModule = resolveModule(moduleExport.from); + if (resolvedModule) { + staticSymbol = + this.resolveExportedSymbol(resolveModule(moduleExport.from), symName); + break; + } } } } @@ -348,10 +369,12 @@ export class StaticReflector implements ReflectorReader { for (const moduleExport of metadata['exports']) { if (!moduleExport.export) { const resolvedModule = resolveModule(moduleExport.from); - const candidateSymbol = this.resolveExportedSymbol(resolvedModule, symbolName); - if (candidateSymbol) { - staticSymbol = candidateSymbol; - break; + if (resolvedModule) { + const candidateSymbol = this.resolveExportedSymbol(resolvedModule, symbolName); + if (candidateSymbol) { + staticSymbol = candidateSymbol; + break; + } } } } @@ -689,7 +712,16 @@ export class StaticReflector implements ReflectorReader { } } - const result = simplifyInContext(context, value, 0); + const recordedSimplifyInContext = (context: StaticSymbol, value: any, depth: number) => { + try { + return simplifyInContext(context, value, depth); + } catch (e) { + this.reportError(e, context); + } + }; + + const result = this.errorRecorder ? recordedSimplifyInContext(context, value, 0) : + simplifyInContext(context, value, 0); if (shouldIgnore(result)) { return undefined; } @@ -717,8 +749,10 @@ export class StaticReflector implements ReflectorReader { {__symbolic: 'module', version: SUPPORTED_SCHEMA_VERSION, module: module, metadata: {}}; } if (moduleMetadata['version'] != SUPPORTED_SCHEMA_VERSION) { - throw new Error( - `Metadata version mismatch for module ${module}, found version ${moduleMetadata['version']}, expected ${SUPPORTED_SCHEMA_VERSION}`); + this.reportError( + new Error( + `Metadata version mismatch for module ${module}, found version ${moduleMetadata['version']}, expected ${SUPPORTED_SCHEMA_VERSION}`), + null); } this.metadataCache.set(module, moduleMetadata); } diff --git a/modules/@angular/language-service/rollup.config.js b/modules/@angular/language-service/rollup.config.js index 0f6fd64828..c9457dfdee 100644 --- a/modules/@angular/language-service/rollup.config.js +++ b/modules/@angular/language-service/rollup.config.js @@ -18,7 +18,7 @@ var locations = { 'tsc-wrapped': normalize('../../../dist/tools/@angular') + '/', }; -var esm_suffixes = {}; +var esm_suffixes = {'compiler-cli': esm}; function normalize(fileName) { return path.resolve(__dirname, fileName); diff --git a/modules/@angular/language-service/src/diagnostics.ts b/modules/@angular/language-service/src/diagnostics.ts index 079e9e52a4..c021208b6b 100644 --- a/modules/@angular/language-service/src/diagnostics.ts +++ b/modules/@angular/language-service/src/diagnostics.ts @@ -55,12 +55,14 @@ export function getDeclarationDiagnostics( let directives: Set|undefined = undefined; for (const declaration of declarations) { - let report = (message: string) => { - results.push( - {kind: DiagnosticKind.Error, span: declaration.declarationSpan, message}); + const report = (message: string, span?: Span) => { + results.push({ + 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) { diff --git a/modules/@angular/language-service/src/reflector_host.ts b/modules/@angular/language-service/src/reflector_host.ts index f4dfc694e9..c84c14e9fc 100644 --- a/modules/@angular/language-service/src/reflector_host.ts +++ b/modules/@angular/language-service/src/reflector_host.ts @@ -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(); - +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(); - - // 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; -} \ No newline at end of file diff --git a/modules/@angular/language-service/src/types.ts b/modules/@angular/language-service/src/types.ts index 3f6dbada28..64bb382fc6 100644 --- a/modules/@angular/language-service/src/types.ts +++ b/modules/@angular/language-service/src/types.ts @@ -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[]; } /** diff --git a/modules/@angular/language-service/src/typescript_host.ts b/modules/@angular/language-service/src/typescript_host.ts index b4e3df6cdb..ff58a9f2fc 100644 --- a/modules/@angular/language-service/src/typescript_host.ts +++ b/modules/@angular/language-service/src/typescript_host.ts @@ -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; private templateReferences: string[]; + private collectedErrors: Map; 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) }; } } diff --git a/modules/@angular/language-service/test/completions_spec.ts b/modules/@angular/language-service/test/completions_spec.ts index fde6f4e62d..db216c4d60 100644 --- a/modules/@angular/language-service/test/completions_spec.ts +++ b/modules/@angular/language-service/test/completions_spec.ts @@ -166,6 +166,7 @@ export class MyComponent { const originalContent = mockHost.getFileContent(fileName); const newContent = originalContent + code; mockHost.override(fileName, originalContent + code); + ngHost.updateAnalyzedModules(); try { cb(fileName, newContent); } finally { diff --git a/modules/@angular/language-service/test/diagnostics_spec.ts b/modules/@angular/language-service/test/diagnostics_spec.ts index cf09934878..a92f519029 100644 --- a/modules/@angular/language-service/test/diagnostics_spec.ts +++ b/modules/@angular/language-service/test/diagnostics_spec.ts @@ -120,6 +120,7 @@ describe('diagnostics', () => { const originalContent = mockHost.getFileContent(fileName); const newContent = originalContent + code; mockHost.override(fileName, originalContent + code); + ngHost.updateAnalyzedModules(); try { cb(fileName, newContent); } finally { diff --git a/modules/@angular/language-service/tsconfig-build.json b/modules/@angular/language-service/tsconfig-build.json index 60921ad99f..fcdbb0db4d 100644 --- a/modules/@angular/language-service/tsconfig-build.json +++ b/modules/@angular/language-service/tsconfig-build.json @@ -15,6 +15,7 @@ "@angular/common": ["../../../dist/packages-dist/common"], "@angular/compiler": ["../../../dist/packages-dist/compiler"], "@angular/compiler/*": ["../../../dist/packages-dist/compiler/*"], + "@angular/compiler-cli": ["../../../dist/packages-dist/compiler-cli"], "@angular/platform-server": ["../../../dist/packages-dist/platform-server"], "@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"], "@angular/tsc-wrapped": ["../../../dist/tools/@angular/tsc-wrapped"],