Skip to content

Commit 646655d

Browse files
AndrewKushnirmhevery
authored andcommitted
fix(compiler): use FatalDiagnosticError to generate better error messages (#35244)
Prior to this commit, decorator handling logic in Ngtsc used `Error` to throw errors. This commit replaces most of these instances with `FatalDiagnosticError` class, which provider a better diagnostics error (including location of the problematic code). PR Close #35244
1 parent df75451 commit 646655d

File tree

3 files changed

+422
-103
lines changed

3 files changed

+422
-103
lines changed

packages/compiler-cli/src/ngtsc/annotations/src/directive.ts

Lines changed: 69 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export function extractDirectiveMetadata(
191191
if (!ts.isObjectLiteralExpression(meta)) {
192192
throw new FatalDiagnosticError(
193193
ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta,
194-
`@${decorator.name} argument must be literal.`);
194+
`@${decorator.name} argument must be an object literal`);
195195
}
196196
directive = reflectObjectLiteral(meta);
197197
}
@@ -345,7 +345,7 @@ export function extractQueryMetadata(
345345
predicate = new WrappedNodeExpr(node);
346346
} else if (typeof arg === 'string') {
347347
predicate = [arg];
348-
} else if (isStringArrayOrDie(arg, '@' + name)) {
348+
} else if (isStringArrayOrDie(arg, `@${name} predicate`, node)) {
349349
predicate = arg;
350350
} else {
351351
throw new FatalDiagnosticError(
@@ -359,17 +359,22 @@ export function extractQueryMetadata(
359359
if (args.length === 2) {
360360
const optionsExpr = unwrapExpression(args[1]);
361361
if (!ts.isObjectLiteralExpression(optionsExpr)) {
362-
throw new Error(`@${name} options must be an object literal`);
362+
throw new FatalDiagnosticError(
363+
ErrorCode.DECORATOR_ARG_NOT_LITERAL, optionsExpr,
364+
`@${name} options must be an object literal`);
363365
}
364366
const options = reflectObjectLiteral(optionsExpr);
365367
if (options.has('read')) {
366368
read = new WrappedNodeExpr(options.get('read') !);
367369
}
368370

369371
if (options.has('descendants')) {
370-
const descendantsValue = evaluator.evaluate(options.get('descendants') !);
372+
const descendantsExpr = options.get('descendants') !;
373+
const descendantsValue = evaluator.evaluate(descendantsExpr);
371374
if (typeof descendantsValue !== 'boolean') {
372-
throw new Error(`@${name} options.descendants must be a boolean`);
375+
throw new FatalDiagnosticError(
376+
ErrorCode.VALUE_HAS_WRONG_TYPE, descendantsExpr,
377+
`@${name} options.descendants must be a boolean`);
373378
}
374379
descendants = descendantsValue;
375380
}
@@ -385,7 +390,8 @@ export function extractQueryMetadata(
385390

386391
} else if (args.length > 2) {
387392
// Too many arguments.
388-
throw new Error(`@${name} has too many arguments`);
393+
throw new FatalDiagnosticError(
394+
ErrorCode.DECORATOR_ARITY_WRONG, node, `@${name} has too many arguments`);
389395
}
390396

391397
return {
@@ -406,17 +412,23 @@ export function extractQueriesFromDecorator(
406412
} {
407413
const content: R3QueryMetadata[] = [], view: R3QueryMetadata[] = [];
408414
if (!ts.isObjectLiteralExpression(queryData)) {
409-
throw new Error(`queries metadata must be an object literal`);
415+
throw new FatalDiagnosticError(
416+
ErrorCode.VALUE_HAS_WRONG_TYPE, queryData,
417+
'Decorator queries metadata must be an object literal');
410418
}
411419
reflectObjectLiteral(queryData).forEach((queryExpr, propertyName) => {
412420
queryExpr = unwrapExpression(queryExpr);
413421
if (!ts.isNewExpression(queryExpr) || !ts.isIdentifier(queryExpr.expression)) {
414-
throw new Error(`query metadata must be an instance of a query type`);
422+
throw new FatalDiagnosticError(
423+
ErrorCode.VALUE_HAS_WRONG_TYPE, queryData,
424+
'Decorator query metadata must be an instance of a query type');
415425
}
416426
const type = reflector.getImportOfIdentifier(queryExpr.expression);
417427
if (type === null || (!isCore && type.from !== '@angular/core') ||
418428
!QUERY_TYPES.has(type.name)) {
419-
throw new Error(`query metadata must be an instance of a query type`);
429+
throw new FatalDiagnosticError(
430+
ErrorCode.VALUE_HAS_WRONG_TYPE, queryData,
431+
'Decorator query metadata must be an instance of a query type');
420432
}
421433

422434
const query = extractQueryMetadata(
@@ -430,14 +442,16 @@ export function extractQueriesFromDecorator(
430442
return {content, view};
431443
}
432444

433-
function isStringArrayOrDie(value: any, name: string): value is string[] {
445+
function isStringArrayOrDie(value: any, name: string, node: ts.Expression): value is string[] {
434446
if (!Array.isArray(value)) {
435447
return false;
436448
}
437449

438450
for (let i = 0; i < value.length; i++) {
439451
if (typeof value[i] !== 'string') {
440-
throw new Error(`Failed to resolve ${name}[${i}] to a string`);
452+
throw new FatalDiagnosticError(
453+
ErrorCode.VALUE_HAS_WRONG_TYPE, node,
454+
`Failed to resolve ${name} at position ${i} to a string`);
441455
}
442456
}
443457
return true;
@@ -451,9 +465,12 @@ export function parseFieldArrayValue(
451465
}
452466

453467
// Resolve the field of interest from the directive metadata to a string[].
454-
const value = evaluator.evaluate(directive.get(field) !);
455-
if (!isStringArrayOrDie(value, field)) {
456-
throw new Error(`Failed to resolve @Directive.${field}`);
468+
const expression = directive.get(field) !;
469+
const value = evaluator.evaluate(expression);
470+
if (!isStringArrayOrDie(value, field, expression)) {
471+
throw new FatalDiagnosticError(
472+
ErrorCode.VALUE_HAS_WRONG_TYPE, expression,
473+
`Failed to resolve @Directive.${field} to a string array`);
457474
}
458475

459476
return value;
@@ -501,13 +518,16 @@ function parseDecoratedFields(
501518
} else if (decorator.args.length === 1) {
502519
const property = evaluator.evaluate(decorator.args[0]);
503520
if (typeof property !== 'string') {
504-
throw new Error(`Decorator argument must resolve to a string`);
521+
throw new FatalDiagnosticError(
522+
ErrorCode.VALUE_HAS_WRONG_TYPE, Decorator.nodeForError(decorator),
523+
`@${decorator.name} decorator argument must resolve to a string`);
505524
}
506525
results[fieldName] = mapValueResolver(property, fieldName);
507526
} else {
508527
// Too many arguments.
509-
throw new Error(
510-
`Decorator must have 0 or 1 arguments, got ${decorator.args.length} argument(s)`);
528+
throw new FatalDiagnosticError(
529+
ErrorCode.DECORATOR_ARITY_WRONG, Decorator.nodeForError(decorator),
530+
`@${decorator.name} can have at most one argument, got ${decorator.args.length} argument(s)`);
511531
}
512532
});
513533
return results;
@@ -568,7 +588,7 @@ export function extractHostBindings(
568588
const hostMetaMap = evaluator.evaluate(expr);
569589
if (!(hostMetaMap instanceof Map)) {
570590
throw new FatalDiagnosticError(
571-
ErrorCode.DECORATOR_ARG_NOT_LITERAL, expr, `Decorator host metadata must be an object`);
591+
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, `Decorator host metadata must be an object`);
572592
}
573593
hostMetaMap.forEach((value, key) => {
574594
// Resolve Enum references to their declared value.
@@ -577,17 +597,19 @@ export function extractHostBindings(
577597
}
578598

579599
if (typeof key !== 'string') {
580-
throw new Error(
581-
`Decorator host metadata must be a string -> string object, but found unparseable key ${key}`);
600+
throw new FatalDiagnosticError(
601+
ErrorCode.VALUE_HAS_WRONG_TYPE, expr,
602+
`Decorator host metadata must be a string -> string object, but found unparseable key`);
582603
}
583604

584605
if (typeof value == 'string') {
585606
hostMetadata[key] = value;
586607
} else if (value instanceof DynamicValue) {
587608
hostMetadata[key] = new WrappedNodeExpr(value.node as ts.Expression);
588609
} else {
589-
throw new Error(
590-
`Decorator host metadata must be a string -> string object, but found unparseable value ${value}`);
610+
throw new FatalDiagnosticError(
611+
ErrorCode.VALUE_HAS_WRONG_TYPE, expr,
612+
`Decorator host metadata must be a string -> string object, but found unparseable value`);
591613
}
592614
});
593615
}
@@ -605,26 +627,29 @@ export function extractHostBindings(
605627
errors.map((error: ParseError) => error.msg).join('\n'));
606628
}
607629

608-
filterToMembersWithDecorator(members, 'HostBinding', coreModule)
609-
.forEach(({member, decorators}) => {
610-
decorators.forEach(decorator => {
611-
let hostPropertyName: string = member.name;
612-
if (decorator.args !== null && decorator.args.length > 0) {
613-
if (decorator.args.length !== 1) {
614-
throw new Error(`@HostBinding() can have at most one argument`);
615-
}
630+
filterToMembersWithDecorator(members, 'HostBinding', coreModule).forEach(({member, decorators}) => {
631+
decorators.forEach(decorator => {
632+
let hostPropertyName: string = member.name;
633+
if (decorator.args !== null && decorator.args.length > 0) {
634+
if (decorator.args.length !== 1) {
635+
throw new FatalDiagnosticError(
636+
ErrorCode.DECORATOR_ARITY_WRONG, Decorator.nodeForError(decorator),
637+
`@HostBinding can have at most one argument, got ${decorator.args.length} argument(s)`);
638+
}
616639

617-
const resolved = evaluator.evaluate(decorator.args[0]);
618-
if (typeof resolved !== 'string') {
619-
throw new Error(`@HostBinding()'s argument must be a string`);
620-
}
640+
const resolved = evaluator.evaluate(decorator.args[0]);
641+
if (typeof resolved !== 'string') {
642+
throw new FatalDiagnosticError(
643+
ErrorCode.VALUE_HAS_WRONG_TYPE, Decorator.nodeForError(decorator),
644+
`@HostBinding's argument must be a string`);
645+
}
621646

622-
hostPropertyName = resolved;
623-
}
647+
hostPropertyName = resolved;
648+
}
624649

625-
bindings.properties[hostPropertyName] = member.name;
626-
});
627-
});
650+
bindings.properties[hostPropertyName] = member.name;
651+
});
652+
});
628653

629654
filterToMembersWithDecorator(members, 'HostListener', coreModule)
630655
.forEach(({member, decorators}) => {
@@ -635,24 +660,25 @@ export function extractHostBindings(
635660
if (decorator.args.length > 2) {
636661
throw new FatalDiagnosticError(
637662
ErrorCode.DECORATOR_ARITY_WRONG, decorator.args[2],
638-
`@HostListener() can have at most two arguments`);
663+
`@HostListener can have at most two arguments`);
639664
}
640665

641666
const resolved = evaluator.evaluate(decorator.args[0]);
642667
if (typeof resolved !== 'string') {
643668
throw new FatalDiagnosticError(
644669
ErrorCode.VALUE_HAS_WRONG_TYPE, decorator.args[0],
645-
`@HostListener()'s event name argument must be a string`);
670+
`@HostListener's event name argument must be a string`);
646671
}
647672

648673
eventName = resolved;
649674

650675
if (decorator.args.length === 2) {
676+
const expression = decorator.args[1];
651677
const resolvedArgs = evaluator.evaluate(decorator.args[1]);
652-
if (!isStringArrayOrDie(resolvedArgs, '@HostListener.args')) {
678+
if (!isStringArrayOrDie(resolvedArgs, '@HostListener.args', expression)) {
653679
throw new FatalDiagnosticError(
654680
ErrorCode.VALUE_HAS_WRONG_TYPE, decorator.args[1],
655-
`@HostListener second argument must be a string array`);
681+
`@HostListener's second argument must be a string array`);
656682
}
657683
args = resolvedArgs;
658684
}

packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,7 @@ export class InjectableDecoratorHandler implements
126126

127127
/**
128128
* Read metadata from the `@Injectable` decorator and produce the `IvyInjectableMetadata`, the
129-
* input
130-
* metadata needed to run `compileIvyInjectable`.
129+
* input metadata needed to run `compileIvyInjectable`.
131130
*
132131
* A `null` return value indicates this is @Injectable has invalid data.
133132
*/
@@ -157,7 +156,9 @@ function extractInjectableMetadata(
157156
// transport references from one location to another. This is the problem that lowering
158157
// used to solve - if this restriction proves too undesirable we can re-implement lowering.
159158
if (!ts.isObjectLiteralExpression(metaNode)) {
160-
throw new Error(`In Ivy, decorator metadata must be inline.`);
159+
throw new FatalDiagnosticError(
160+
ErrorCode.DECORATOR_ARG_NOT_LITERAL, metaNode,
161+
`@Injectable argument must be an object literal`);
161162
}
162163

163164
// Resolve the fields of the literal into a map of field name to expression.
@@ -173,7 +174,7 @@ function extractInjectableMetadata(
173174
if (!ts.isArrayLiteralExpression(depsExpr)) {
174175
throw new FatalDiagnosticError(
175176
ErrorCode.VALUE_NOT_LITERAL, depsExpr,
176-
`In Ivy, deps metadata must be an inline array.`);
177+
`@Injectable deps metadata must be an inline array`);
177178
}
178179
userDeps = depsExpr.elements.map(dep => getDep(dep, reflector));
179180
}

0 commit comments

Comments
 (0)