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
This commit is contained in:
Alex Rickabaugh 2018-08-23 14:34:55 -07:00 committed by Misko Hevery
parent b424b3187e
commit 38f624d7e3
19 changed files with 331 additions and 85 deletions

View File

@ -116,7 +116,7 @@ export interface Program {
getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
ReadonlyArray<ts.Diagnostic>;
getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken):
ReadonlyArray<Diagnostic>;
ReadonlyArray<ts.Diagnostic|Diagnostic>;
loadNgStructureAsync(): Promise<void>;
listLazyRoutes(entryRoute?: string): LazyRoute[];
emit({emitFlags, cancellationToken, customTransformers, emitCallback}: {

View File

@ -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",

View File

@ -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<R3ComponentMe
const component = reflectObjectLiteral(meta);
if (this.resourceLoader.preload !== undefined && component.has('templateUrl')) {
const templateUrl =
staticallyResolve(component.get('templateUrl') !, this.reflector, this.checker);
const templateUrlExpr = component.get('templateUrl') !;
const templateUrl = staticallyResolve(templateUrlExpr, this.reflector, this.checker);
if (typeof templateUrl !== 'string') {
throw new Error(`templateUrl should be a string`);
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string');
}
const url = path.posix.resolve(path.dirname(node.getSourceFile().fileName), templateUrl);
return this.resourceLoader.preload(url);
@ -77,10 +79,11 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
let templateStr: string|null = null;
if (component.has('templateUrl')) {
const templateUrl =
staticallyResolve(component.get('templateUrl') !, this.reflector, this.checker);
const templateUrlExpr = component.get('templateUrl') !;
const templateUrl = staticallyResolve(templateUrlExpr, this.reflector, this.checker);
if (typeof templateUrl !== 'string') {
throw new Error(`templateUrl should be a string`);
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string');
}
const url = path.posix.resolve(path.dirname(node.getSourceFile().fileName), templateUrl);
templateStr = this.resourceLoader.load(url);
@ -88,19 +91,22 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
const templateExpr = component.get('template') !;
const resolvedTemplate = staticallyResolve(templateExpr, this.reflector, this.checker);
if (typeof resolvedTemplate !== 'string') {
throw new Error(`Template must statically resolve to a string: ${node.name!.text}`);
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateExpr, 'template must be a string');
}
templateStr = resolvedTemplate;
} else {
throw new Error(`Component has no template or templateUrl`);
throw new FatalDiagnosticError(
ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node, 'component is missing a template');
}
let preserveWhitespaces: boolean = false;
if (component.has('preserveWhitespaces')) {
const value =
staticallyResolve(component.get('preserveWhitespaces') !, this.reflector, this.checker);
const expr = component.get('preserveWhitespaces') !;
const value = staticallyResolve(expr, this.reflector, this.checker);
if (typeof value !== 'boolean') {
throw new Error(`preserveWhitespaces must resolve to a boolean if present`);
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, 'preserveWhitespaces must be a boolean');
}
preserveWhitespaces = value;
}
@ -191,12 +197,15 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
return this.literalCache.get(decorator) !;
}
if (decorator.args === null || decorator.args.length !== 1) {
throw new Error(`Incorrect number of arguments to @Component decorator`);
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_ARITY_WRONG, decorator.node,
`Incorrect number of arguments to @Component decorator`);
}
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, `Decorator argument must be literal.`);
}
this.literalCache.set(decorator, meta);

View File

@ -9,6 +9,7 @@
import {ConstantPool, Expression, R3DirectiveMetadata, R3QueryMetadata, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser, parseHostBindings} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {ClassMember, ClassMemberKind, Decorator, Import, ReflectionHost} from '../../host';
import {Reference, filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
@ -68,11 +69,14 @@ export function extractDirectiveMetadata(
decoratedElements: ClassMember[],
}|undefined {
if (decorator.args === null || decorator.args.length !== 1) {
throw new Error(`Incorrect number of arguments to @${decorator.name} decorator`);
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_ARITY_WRONG, decorator.node,
`Incorrect number of arguments to @${decorator.name} decorator`);
}
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, `@${decorator.name} argument must be literal.`);
}
const directive = reflectObjectLiteral(meta);
@ -120,9 +124,11 @@ export function extractDirectiveMetadata(
// Parse the selector.
let selector = '';
if (directive.has('selector')) {
const resolved = staticallyResolve(directive.get('selector') !, reflector, checker);
const expr = directive.get('selector') !;
const resolved = staticallyResolve(expr, reflector, checker);
if (typeof resolved !== 'string') {
throw new Error(`Selector must be a string`);
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, `selector must be a string`);
}
selector = resolved;
}
@ -137,9 +143,11 @@ export function extractDirectiveMetadata(
// Parse exportAs.
let exportAs: string|null = null;
if (directive.has('exportAs')) {
const resolved = staticallyResolve(directive.get('exportAs') !, reflector, checker);
const expr = directive.get('exportAs') !;
const resolved = staticallyResolve(expr, reflector, checker);
if (typeof resolved !== 'string') {
throw new Error(`exportAs must be a string`);
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, `exportAs must be a string`);
}
exportAs = resolved;
}
@ -163,10 +171,11 @@ export function extractDirectiveMetadata(
}
export function extractQueryMetadata(
name: string, args: ReadonlyArray<ts.Expression>, propertyName: string,
exprNode: ts.Node, name: string, args: ReadonlyArray<ts.Expression>, 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;
}

View File

@ -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');
}
}

View File

@ -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<NgModuleAnalys
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<NgModuleAnalysis> {
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<NgModuleAnalys
ts.createObjectLiteral([]);
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta,
'@NgModule argument must be an object literal');
}
const ngModule = reflectObjectLiteral(meta);
@ -62,23 +67,25 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
// Extract the module declarations, imports, and exports.
let declarations: Reference[] = [];
if (ngModule.has('declarations')) {
const declarationMeta =
staticallyResolve(ngModule.get('declarations') !, this.reflector, this.checker);
declarations = this.resolveTypeList(declarationMeta, 'declarations');
const expr = ngModule.get('declarations') !;
const declarationMeta = staticallyResolve(expr, this.reflector, this.checker);
declarations = this.resolveTypeList(expr, declarationMeta, 'declarations');
}
let imports: Reference[] = [];
if (ngModule.has('imports')) {
const expr = ngModule.get('imports') !;
const importsMeta = staticallyResolve(
ngModule.get('imports') !, this.reflector, this.checker,
expr, this.reflector, this.checker,
ref => 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<NgModuleAnalys
/**
* Compute a list of `Reference`s from a resolved metadata value.
*/
private resolveTypeList(resolvedList: ResolvedValue, name: string): Reference[] {
private resolveTypeList(expr: ts.Node, resolvedList: ResolvedValue, name: string): Reference[] {
const refList: Reference[] = [];
if (!Array.isArray(resolvedList)) {
throw new Error(`Expected array when reading property ${name}`);
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, `Expected array when reading property ${name}`);
}
resolvedList.forEach((entry, idx) => {
@ -200,12 +208,15 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
if (Array.isArray(entry)) {
// Recurse into nested arrays.
refList.push(...this.resolveTypeList(entry, name));
refList.push(...this.resolveTypeList(expr, entry, name));
} else if (entry instanceof Reference) {
if (!entry.expressable) {
throw new Error(`Value at position ${idx} in ${name} array is not expressable`);
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, `One entry in ${name} is not a type`);
} else if (!this.reflector.isClass(entry.node)) {
throw new Error(`Value at position ${idx} in ${name} array is not a class declaration`);
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, entry.node,
`Entry is not a type, but is used as such in ${name} array`);
}
refList.push(entry);
} else {

View File

@ -9,6 +9,7 @@
import {LiteralExpr, R3PipeMetadata, WrappedNodeExpr, compilePipeFromMetadata} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {Decorator, ReflectionHost} from '../../host';
import {reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
@ -31,33 +32,45 @@ export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata, De
analyze(clazz: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3PipeMetadata> {
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;
}

View File

@ -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)) {

View File

@ -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",
],

View File

@ -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());
}

View File

@ -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",
],
)

View File

@ -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';

View File

@ -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,
}

View File

@ -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;
}

View File

@ -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');
}

View File

@ -88,9 +88,10 @@ export class NgtscProgram implements api.Program {
}
getNgSemanticDiagnostics(
fileName?: string|undefined,
cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray<api.Diagnostic> {
return [];
fileName?: string|undefined, cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<ts.Diagnostic|api.Diagnostic> {
const compilation = this.ensureAnalyzed();
return compilation.diagnostics;
}
async loadNgStructureAsync(): Promise<void> {
@ -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 =

View File

@ -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",

View File

@ -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;
}
}
};

View File

@ -340,7 +340,7 @@ export interface Program {
* Angular structural information is required to produce these diagnostics.
*/
getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken):
ReadonlyArray<Diagnostic>;
ReadonlyArray<ts.Diagnostic|Diagnostic>;
/**
* Load Angular structural information asynchronously. If this method is not called then the