From 38f624d7e3c4d39b618bef37a4f0bbb9500dc3f3 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Thu, 23 Aug 2018 14:34:55 -0700 Subject: [PATCH] feat(ivy): output diagnostics for many errors in ngtsc (#25647) This commit takes the first steps towards ngtsc producing real TypeScript diagnostics instead of simply throwing errors when encountering incorrect code. A new class is introduced, FatalDiagnosticError, which can be thrown by handlers whenever a condition in the code is encountered which by necessity prevents the class from being compiled. This error type is convertable to a ts.Diagnostic which represents the type and source of the error. Error codes are introduced for Angular errors, and are prefixed with -99 (so error code 1001 becomes -991001) to distinguish them from other TS errors. A function is provided which will read TS diagnostic output and convert the TS errors to NG errors if they match this negative error code format. PR Close #25647 --- packages/compiler-cli/src/ngtools_api2.ts | 2 +- .../src/ngtsc/annotations/BUILD.bazel | 1 + .../src/ngtsc/annotations/src/component.ts | 35 ++++++---- .../src/ngtsc/annotations/src/directive.ts | 50 +++++++++----- .../src/ngtsc/annotations/src/injectable.ts | 14 ++-- .../src/ngtsc/annotations/src/ng_module.ts | 39 +++++++---- .../src/ngtsc/annotations/src/pipe.ts | 29 ++++++--- .../src/ngtsc/annotations/src/util.ts | 16 +++-- .../src/ngtsc/annotations/test/BUILD.bazel | 1 + .../ngtsc/annotations/test/component_spec.ts | 65 +++++++++++++++++++ .../src/ngtsc/diagnostics/BUILD.bazel | 15 +++++ .../src/ngtsc/diagnostics/index.ts | 11 ++++ .../src/ngtsc/diagnostics/src/code.ts | 22 +++++++ .../src/ngtsc/diagnostics/src/error.ts | 36 ++++++++++ .../src/ngtsc/diagnostics/src/util.ts | 15 +++++ packages/compiler-cli/src/ngtsc/program.ts | 24 ++++--- .../src/ngtsc/transform/BUILD.bazel | 1 + .../src/ngtsc/transform/src/compilation.ts | 38 ++++++----- packages/compiler-cli/src/transformers/api.ts | 2 +- 19 files changed, 331 insertions(+), 85 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts create mode 100644 packages/compiler-cli/src/ngtsc/diagnostics/BUILD.bazel create mode 100644 packages/compiler-cli/src/ngtsc/diagnostics/index.ts create mode 100644 packages/compiler-cli/src/ngtsc/diagnostics/src/code.ts create mode 100644 packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts create mode 100644 packages/compiler-cli/src/ngtsc/diagnostics/src/util.ts diff --git a/packages/compiler-cli/src/ngtools_api2.ts b/packages/compiler-cli/src/ngtools_api2.ts index 956f34ab27..eef681486c 100644 --- a/packages/compiler-cli/src/ngtools_api2.ts +++ b/packages/compiler-cli/src/ngtools_api2.ts @@ -116,7 +116,7 @@ export interface Program { getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): ReadonlyArray; getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken): - ReadonlyArray; + ReadonlyArray; loadNgStructureAsync(): Promise; listLazyRoutes(entryRoute?: string): LazyRoute[]; emit({emitFlags, cancellationToken, customTransformers, emitCallback}: { diff --git a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel index 4a0cd4bb59..f5d623ea0e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( module_name = "@angular/compiler-cli/src/ngtsc/annotations", deps = [ "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/host", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/transform", diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index f71deaf16a..a805467676 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -10,6 +10,7 @@ import {ConstantPool, Expression, R3ComponentMetadata, R3DirectiveMetadata, Wrap import * as path from 'path'; import * as ts from 'typescript'; +import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {Decorator, ReflectionHost} from '../../host'; import {filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata'; import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; @@ -46,10 +47,11 @@ export class ComponentDecoratorHandler implements DecoratorHandler, propertyName: string, + exprNode: ts.Node, name: string, args: ReadonlyArray, propertyName: string, reflector: ReflectionHost, checker: ts.TypeChecker): R3QueryMetadata { if (args.length === 0) { - throw new Error(`@${name} must have arguments`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARITY_WRONG, exprNode, `@${name} must have arguments`); } const first = name === 'ViewChild' || name === 'ContentChild'; const node = unwrapForwardRef(args[0], reflector); @@ -181,7 +190,8 @@ export function extractQueryMetadata( } else if (isStringArrayOrDie(arg, '@' + name)) { predicate = arg as string[]; } else { - throw new Error(`@${name} predicate cannot be interpreted`); + throw new FatalDiagnosticError( + ErrorCode.VALUE_HAS_WRONG_TYPE, node, `@${name} predicate cannot be interpreted`); } // Extract the read and descendants options. @@ -238,7 +248,7 @@ export function extractQueriesFromDecorator( } const query = extractQueryMetadata( - type.name, queryExpr.arguments || [], propertyName, reflector, checker); + queryExpr, type.name, queryExpr.arguments || [], propertyName, reflector, checker); if (type.name.startsWith('Content')) { content.push(query); } else { @@ -343,7 +353,7 @@ export function queriesFromFields( } const decorator = decorators[0]; return extractQueryMetadata( - decorator.name, decorator.args || [], member.name, reflector, checker); + decorator.node, decorator.name, decorator.args || [], member.name, reflector, checker); }); } @@ -365,9 +375,11 @@ function extractHostBindings( } { let hostMetadata: StringMap = {}; if (metadata.has('host')) { - const hostMetaMap = staticallyResolve(metadata.get('host') !, reflector, checker); + const expr = metadata.get('host') !; + const hostMetaMap = staticallyResolve(expr, reflector, checker); if (!(hostMetaMap instanceof Map)) { - throw new Error(`Decorator host metadata must be an object`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARG_NOT_LITERAL, expr, `Decorator host metadata must be an object`); } hostMetaMap.forEach((value, key) => { if (typeof value !== 'string' || typeof key !== 'string') { @@ -407,12 +419,16 @@ function extractHostBindings( let args: string[] = []; if (decorator.args !== null && decorator.args.length > 0) { if (decorator.args.length > 2) { - throw new Error(`@HostListener() can have at most two arguments`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARITY_WRONG, decorator.args[2], + `@HostListener() can have at most two arguments`); } const resolved = staticallyResolve(decorator.args[0], reflector, checker); if (typeof resolved !== 'string') { - throw new Error(`@HostListener()'s event name argument must be a string`); + throw new FatalDiagnosticError( + ErrorCode.VALUE_HAS_WRONG_TYPE, decorator.args[0], + `@HostListener()'s event name argument must be a string`); } eventName = resolved; @@ -420,7 +436,9 @@ function extractHostBindings( if (decorator.args.length === 2) { const resolvedArgs = staticallyResolve(decorator.args[1], reflector, checker); if (!isStringArrayOrDie(resolvedArgs, '@HostListener.args')) { - throw new Error(`@HostListener second argument must be a string array`); + throw new FatalDiagnosticError( + ErrorCode.VALUE_HAS_WRONG_TYPE, decorator.args[1], + `@HostListener second argument must be a string array`); } args = resolvedArgs; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index 65711ed3f3..d39993fab8 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts @@ -9,6 +9,7 @@ import {Expression, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler'; import * as ts from 'typescript'; +import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {Decorator, ReflectionHost} from '../../host'; import {reflectObjectLiteral} from '../../metadata'; import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; @@ -56,13 +57,15 @@ function extractInjectableMetadata( clazz: ts.ClassDeclaration, decorator: Decorator, reflector: ReflectionHost, isCore: boolean): R3InjectableMetadata { if (clazz.name === undefined) { - throw new Error(`@Injectables must have names`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ON_ANONYMOUS_CLASS, decorator.node, `@Injectable on anonymous class`); } const name = clazz.name.text; const type = new WrappedNodeExpr(clazz.name); const ctorDeps = getConstructorDependencies(clazz, reflector, isCore); if (decorator.args === null) { - throw new Error(`@Injectable must be called`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_NOT_CALLED, decorator.node, '@Injectable must be called'); } if (decorator.args.length === 0) { return { @@ -90,7 +93,9 @@ function extractInjectableMetadata( if ((meta.has('useClass') || meta.has('useFactory')) && meta.has('deps')) { const depsExpr = meta.get('deps') !; if (!ts.isArrayLiteralExpression(depsExpr)) { - throw new Error(`In Ivy, deps metadata must be inline.`); + throw new FatalDiagnosticError( + ErrorCode.VALUE_NOT_LITERAL, depsExpr, + `In Ivy, deps metadata must be an inline array.`); } if (depsExpr.elements.length > 0) { throw new Error(`deps not yet supported`); @@ -130,7 +135,8 @@ function extractInjectableMetadata( return {name, type, providedIn, ctorDeps}; } } else { - throw new Error(`Too many arguments to @Injectable`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARITY_WRONG, decorator.args[2], 'Too many arguments to @Injectable'); } } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index 4d3a0e92e2..eb9374f364 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -9,6 +9,7 @@ import {ConstantPool, Expression, LiteralArrayExpr, R3DirectiveMetadata, R3InjectorMetadata, R3NgModuleMetadata, WrappedNodeExpr, compileInjector, compileNgModule, makeBindingParser, parseTemplate} from '@angular/compiler'; import * as ts from 'typescript'; +import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {Decorator, ReflectionHost} from '../../host'; import {Reference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from '../../metadata'; import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; @@ -41,7 +42,9 @@ export class NgModuleDecoratorHandler implements DecoratorHandler { if (decorator.args === null || decorator.args.length > 1) { - throw new Error(`Incorrect number of arguments to @NgModule decorator`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, + `Incorrect number of arguments to @NgModule decorator`); } // @NgModule can be invoked without arguments. In case it is, pretend as if a blank object @@ -50,7 +53,9 @@ export class NgModuleDecoratorHandler implements DecoratorHandler this._extractModuleFromModuleWithProvidersFn(ref.node)); - imports = this.resolveTypeList(importsMeta, 'imports'); + imports = this.resolveTypeList(expr, importsMeta, 'imports'); } let exports: Reference[] = []; if (ngModule.has('exports')) { + const expr = ngModule.get('exports') !; const exportsMeta = staticallyResolve( - ngModule.get('exports') !, this.reflector, this.checker, + expr, this.reflector, this.checker, ref => this._extractModuleFromModuleWithProvidersFn(ref.node)); - exports = this.resolveTypeList(exportsMeta, 'exports'); + exports = this.resolveTypeList(expr, exportsMeta, 'exports'); } // Register this module's information with the SelectorScopeRegistry. This ensures that during @@ -185,10 +192,11 @@ export class NgModuleDecoratorHandler implements DecoratorHandler { @@ -200,12 +208,15 @@ export class NgModuleDecoratorHandler implements DecoratorHandler { if (clazz.name === undefined) { - throw new Error(`@Pipes must have names`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ON_ANONYMOUS_CLASS, clazz, `@Pipes must have names`); } const name = clazz.name.text; const type = new WrappedNodeExpr(clazz.name); if (decorator.args === null) { - throw new Error(`@Pipe must be called`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_NOT_CALLED, decorator.node, `@Pipe must be called`); + } + if (decorator.args.length !== 1) { + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, '@Pipe must have exactly one argument'); } const meta = unwrapExpression(decorator.args[0]); if (!ts.isObjectLiteralExpression(meta)) { - throw new Error(`Decorator argument must be literal.`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta, '@Pipe must have a literal argument'); } const pipe = reflectObjectLiteral(meta); if (!pipe.has('name')) { - throw new Error(`@Pipe decorator is missing name field`); + throw new FatalDiagnosticError( + ErrorCode.PIPE_MISSING_NAME, meta, `@Pipe decorator is missing name field`); } - const pipeName = staticallyResolve(pipe.get('name') !, this.reflector, this.checker); + const pipeNameExpr = pipe.get('name') !; + const pipeName = staticallyResolve(pipeNameExpr, this.reflector, this.checker); if (typeof pipeName !== 'string') { - throw new Error(`@Pipe.name must be a string`); + throw new FatalDiagnosticError( + ErrorCode.VALUE_HAS_WRONG_TYPE, pipeNameExpr, `@Pipe.name must be a string`); } this.scopeRegistry.registerPipe(clazz, pipeName); let pure = true; if (pipe.has('pure')) { - const pureValue = staticallyResolve(pipe.get('pure') !, this.reflector, this.checker); + const expr = pipe.get('pure') !; + const pureValue = staticallyResolve(expr, this.reflector, this.checker); if (typeof pureValue !== 'boolean') { - throw new Error(`@Pipe.pure must be a boolean`); + throw new FatalDiagnosticError( + ErrorCode.VALUE_HAS_WRONG_TYPE, expr, `@Pipe.pure must be a boolean`); } pure = pureValue; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 33cc471d92..fe11c90ea3 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -9,6 +9,7 @@ import {Expression, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; +import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {Decorator, ReflectionHost} from '../../host'; import {AbsoluteReference, ImportMode, Reference} from '../../metadata'; @@ -31,7 +32,9 @@ export function getConstructorDependencies( (param.decorators || []).filter(dec => isCore || isAngularCore(dec)).forEach(dec => { if (dec.name === 'Inject') { if (dec.args === null || dec.args.length !== 1) { - throw new Error(`Unexpected number of arguments to @Inject().`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARITY_WRONG, dec.node, + `Unexpected number of arguments to @Inject().`); } tokenExpr = dec.args[0]; } else if (dec.name === 'Optional') { @@ -44,16 +47,21 @@ export function getConstructorDependencies( host = true; } else if (dec.name === 'Attribute') { if (dec.args === null || dec.args.length !== 1) { - throw new Error(`Unexpected number of arguments to @Attribute().`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARITY_WRONG, dec.node, + `Unexpected number of arguments to @Attribute().`); } tokenExpr = dec.args[0]; resolved = R3ResolvedDependencyType.Attribute; } else { - throw new Error(`Unexpected decorator ${dec.name} on parameter.`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_UNEXPECTED, dec.node, + `Unexpected decorator ${dec.name} on parameter.`); } }); if (tokenExpr === null) { - throw new Error( + throw new FatalDiagnosticError( + ErrorCode.PARAM_MISSING_TOKEN, param.nameNode, `No suitable token for parameter ${param.name || idx} of class ${clazz.name!.text}`); } if (ts.isIdentifier(tokenExpr)) { diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel index a6898c53fb..156ce95512 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel @@ -13,6 +13,7 @@ ts_library( "//packages:types", "//packages/compiler", "//packages/compiler-cli/src/ngtsc/annotations", + "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/testing", ], diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts new file mode 100644 index 0000000000..8c235b0202 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; +import {TypeScriptReflectionHost} from '../../metadata'; +import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {ResourceLoader} from '../src/api'; +import {ComponentDecoratorHandler} from '../src/component'; +import {SelectorScopeRegistry} from '../src/selector_scope'; + +export class NoopResourceLoader implements ResourceLoader { + load(url: string): string { throw new Error('Not implemented'); } +} + +describe('ComponentDecoratorHandler', () => { + it('should produce a diagnostic when @Component has non-literal argument', () => { + const {program} = makeProgram([ + { + name: 'node_modules/@angular/core/index.d.ts', + contents: 'export const Component: any;', + }, + { + name: 'entry.ts', + contents: ` + import {Component} from '@angular/core'; + + const TEST = ''; + @Component(TEST) class TestCmp {} + ` + }, + ]); + const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); + const handler = new ComponentDecoratorHandler( + checker, host, new SelectorScopeRegistry(checker, host), false, new NoopResourceLoader()); + const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', ts.isClassDeclaration); + const detected = handler.detect(TestCmp, host.getDecoratorsOfDeclaration(TestCmp)); + if (detected === undefined) { + return fail('Failed to recognize @Component'); + } + try { + handler.analyze(TestCmp, detected); + return fail('Analysis should have failed'); + } catch (err) { + if (!(err instanceof FatalDiagnosticError)) { + return fail('Error should be a FatalDiagnosticError'); + } + const diag = err.toDiagnostic(); + expect(diag.code).toEqual(ivyCode(ErrorCode.DECORATOR_ARG_NOT_LITERAL)); + expect(diag.file.fileName.endsWith('entry.ts')).toBe(true); + expect(diag.start).toBe(detected.args ![0].getStart()); + } + }); +}); + +function ivyCode(code: ErrorCode): number { + return Number('-99' + code.valueOf()); +} diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/BUILD.bazel b/packages/compiler-cli/src/ngtsc/diagnostics/BUILD.bazel new file mode 100644 index 0000000000..facec10627 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/diagnostics/BUILD.bazel @@ -0,0 +1,15 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "diagnostics", + srcs = glob([ + "index.ts", + "src/**/*.ts", + ]), + module_name = "@angular/compiler-cli/src/ngtsc/diagnostics", + deps = [ + "//packages/compiler", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/index.ts b/packages/compiler-cli/src/ngtsc/diagnostics/index.ts new file mode 100644 index 0000000000..c235c43985 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/diagnostics/index.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export {ErrorCode} from './src/code'; +export {FatalDiagnosticError, isFatalDiagnosticError} from './src/error'; +export {replaceTsWithNgInErrors} from './src/util'; diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/code.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/code.ts new file mode 100644 index 0000000000..679e7f6299 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/code.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export enum ErrorCode { + DECORATOR_ARG_NOT_LITERAL = 1001, + DECORATOR_ARITY_WRONG = 1002, + DECORATOR_NOT_CALLED = 1003, + DECORATOR_ON_ANONYMOUS_CLASS = 1004, + DECORATOR_UNEXPECTED = 1005, + + VALUE_HAS_WRONG_TYPE = 1010, + VALUE_NOT_LITERAL = 1011, + + COMPONENT_MISSING_TEMPLATE = 2001, + PIPE_MISSING_NAME = 2002, + PARAM_MISSING_TOKEN = 2003, +} diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts new file mode 100644 index 0000000000..a6cdd52402 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {ErrorCode} from './code'; + +export class FatalDiagnosticError { + constructor(readonly code: ErrorCode, readonly node: ts.Node, readonly message: string) {} + + /** + * @internal + */ + _isFatalDiagnosticError = true; + + toDiagnostic(): ts.DiagnosticWithLocation { + const node = ts.getOriginalNode(this.node); + return { + category: ts.DiagnosticCategory.Error, + code: Number('-99' + this.code.valueOf()), + file: ts.getOriginalNode(this.node).getSourceFile(), + start: node.getStart(undefined, false), + length: node.getWidth(), + messageText: this.message, + }; + } +} + +export function isFatalDiagnosticError(err: any): err is FatalDiagnosticError { + return err._isFatalDiagnosticError === true; +} diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/util.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/util.ts new file mode 100644 index 0000000000..413fd4bc03 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/util.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +const ERROR_CODE_MATCHER = /(\u001b\[\d+m ?)TS-99(\d+: ?\u001b\[\d+m)/g; + +export function replaceTsWithNgInErrors(errors: string): string { + return errors.replace(ERROR_CODE_MATCHER, '$1NG$2'); +} diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 84d6919c4a..ecae966b34 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -88,9 +88,10 @@ export class NgtscProgram implements api.Program { } getNgSemanticDiagnostics( - fileName?: string|undefined, - cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray { - return []; + fileName?: string|undefined, cancellationToken?: ts.CancellationToken| + undefined): ReadonlyArray { + const compilation = this.ensureAnalyzed(); + return compilation.diagnostics; } async loadNgStructureAsync(): Promise { @@ -117,6 +118,16 @@ export class NgtscProgram implements api.Program { throw new Error('Method not implemented.'); } + private ensureAnalyzed(): IvyCompilation { + if (this.compilation === undefined) { + this.compilation = this.makeCompilation(); + this.tsProgram.getSourceFiles() + .filter(file => !file.fileName.endsWith('.d.ts')) + .forEach(file => this.compilation !.analyzeSync(file)); + } + return this.compilation; + } + emit(opts?: { emitFlags?: api.EmitFlags, cancellationToken?: ts.CancellationToken, @@ -126,12 +137,7 @@ export class NgtscProgram implements api.Program { }): ts.EmitResult { const emitCallback = opts && opts.emitCallback || defaultEmitCallback; - if (this.compilation === undefined) { - this.compilation = this.makeCompilation(); - this.tsProgram.getSourceFiles() - .filter(file => !file.fileName.endsWith('.d.ts')) - .forEach(file => this.compilation !.analyzeSync(file)); - } + this.ensureAnalyzed(); // Since there is no .d.ts transformation API, .d.ts files are transformed during write. const writeFile: ts.WriteFileCallback = diff --git a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel index a23b780019..8456597f7a 100644 --- a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( module_name = "@angular/compiler-cli/src/ngtsc/transform", deps = [ "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/host", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/util", diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index 318c2822a4..e8f7ca2ec0 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -9,6 +9,7 @@ import {ConstantPool} from '@angular/compiler'; import * as ts from 'typescript'; +import {FatalDiagnosticError} from '../../diagnostics'; import {Decorator, ReflectionHost} from '../../host'; import {reflectNameOfDeclaration} from '../../metadata/src/reflector'; @@ -98,23 +99,30 @@ export class IvyCompilation { // Run analysis on the metadata. This will produce either diagnostics, an // analysis result, or both. - const analysis = adapter.analyze(node, metadata); + try { + const analysis = adapter.analyze(node, metadata); + if (analysis.analysis !== undefined) { + this.analysis.set(node, { + adapter, + analysis: analysis.analysis, + metadata: metadata, + }); + } - if (analysis.analysis !== undefined) { - this.analysis.set(node, { - adapter, - analysis: analysis.analysis, - metadata: metadata, - }); - } + if (analysis.diagnostics !== undefined) { + this._diagnostics.push(...analysis.diagnostics); + } - if (analysis.diagnostics !== undefined) { - this._diagnostics.push(...analysis.diagnostics); - } - - if (analysis.factorySymbolName !== undefined && this.sourceToFactorySymbols !== null && - this.sourceToFactorySymbols.has(sf.fileName)) { - this.sourceToFactorySymbols.get(sf.fileName) !.add(analysis.factorySymbolName); + if (analysis.factorySymbolName !== undefined && this.sourceToFactorySymbols !== null && + this.sourceToFactorySymbols.has(sf.fileName)) { + this.sourceToFactorySymbols.get(sf.fileName) !.add(analysis.factorySymbolName); + } + } catch (err) { + if (err instanceof FatalDiagnosticError) { + this._diagnostics.push(err.toDiagnostic()); + } else { + throw err; + } } }; diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index afa15cf20b..f0c202889d 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -340,7 +340,7 @@ export interface Program { * Angular structural information is required to produce these diagnostics. */ getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken): - ReadonlyArray; + ReadonlyArray; /** * Load Angular structural information asynchronously. If this method is not called then the