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:

committed by
Misko Hevery

parent
b424b3187e
commit
38f624d7e3
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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",
|
||||
],
|
||||
|
@ -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());
|
||||
}
|
Reference in New Issue
Block a user