refactor(core): add top 10 runtime error codes (#39188)

adds RuntimeError and code enum to improve debugging experience
refactor ExpressionChangedAfterItHasBeenCheckedError to code NG0100
refactor CyclicDependency to code NG0200
refactor No Provider to code NG0201
refactor MultipleComponentsMatch to code NG0300
refactor ExportNotFound to code NG0301
refactor PipeNotFound to code NG0302
refactor BindingNotKnown to code NG0303
refactor NotKnownElement to code NG0304

PR Close #39188
This commit is contained in:
twerske 2020-10-26 15:45:31 -07:00 committed by Alex Rickabaugh
parent 0723331b2a
commit e6ca3d3841
17 changed files with 101 additions and 26 deletions

View File

@ -196,7 +196,7 @@ which *is* what parent means.
2. Angular throws a cyclic dependency error if you omit the `@SkipSelf` decorator.
`Circular dependency in DI detected for BethComponent. Dependency path: BethComponent -> Parent -> BethComponent`
`NG0200: Circular dependency in DI detected for BethComponent. Dependency path: BethComponent -> Parent -> BethComponent`
Here's *Alice*, *Barry*, and family in action.

View File

@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 3037,
"main-es2015": 448085,
"main-es2015": 448615,
"polyfills-es2015": 52415
}
}

View File

@ -7,6 +7,7 @@
*/
import {forwardRef, Injectable, InjectionToken, Injector, NgModule, ɵcreateInjector as createInjector} from '@angular/core';
import {ivyEnabled} from '@angular/private/testing';
import {AOT_TOKEN, AotModule, AotService} from 'app_built/src/module';
describe('Ivy NgModule', () => {
@ -61,9 +62,10 @@ describe('Ivy NgModule', () => {
class BModule {
}
const errorCode = ivyEnabled ? 'NG0200: ' : '';
expect(() => createInjector(AModule))
.toThrowError(
'Circular dependency in DI detected for AModule. Dependency path: AModule > BModule > AModule');
.toThrowError(`${
errorCode}Circular dependency in DI detected for AModule. Dependency path: AModule > BModule > AModule`);
});
it('merges imports and exports', () => {

View File

@ -0,0 +1,45 @@
/**
* @license
* Copyright Google LLC 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 const enum RuntimeErrorCode {
// Internal Errors
// Change Detection Errors
EXPRESSION_CHANGED_AFTER_CHECKED = '100',
// Dependency Injection Errors
CYCLIC_DI_DEPENDENCY = '200',
PROVIDER_NOT_FOUND = '201',
// Template Errors
MULTIPLE_COMPONENTS_MATCH = '300',
EXPORT_NOT_FOUND = '301',
PIPE_NOT_FOUND = '302',
UNKNOWN_BINDING = '303',
UNKNOWN_ELEMENT = '304',
// Styling Errors
// Declarations Errors
// i18n Errors
// Compilation Errors
}
export class RuntimeError extends Error {
constructor(public code: RuntimeErrorCode, message: string) {
super(formatRuntimeError(code, message));
}
}
/** Called to format a runtime error */
export function formatRuntimeError(code: RuntimeErrorCode, message: string): string {
const fullCode = code ? `NG0${code}: ` : '';
return `${fullCode}${message}`;
}

View File

@ -8,6 +8,7 @@
*/
import {InjectorType} from '../di/interface/defs';
import {stringify} from '../util/stringify';
import {RuntimeError, RuntimeErrorCode} from './error_code';
import {TNode} from './interfaces/node';
import {LView, TVIEW} from './interfaces/view';
@ -18,12 +19,16 @@ import {INTERPOLATION_DELIMITER, stringifyForError} from './util/misc_utils';
/** Called when directives inject each other (creating a circular dependency) */
export function throwCyclicDependencyError(token: string, path?: string[]): never {
const depPath = path ? `. Dependency path: ${path.join(' > ')} > ${token}` : '';
throw new Error(`Circular dependency in DI detected for ${token}${depPath}`);
throw new RuntimeError(
RuntimeErrorCode.CYCLIC_DI_DEPENDENCY,
`Circular dependency in DI detected for ${token}${depPath}`);
}
/** Called when there are multiple component selectors that match a given node */
export function throwMultipleComponentError(tNode: TNode): never {
throw new Error(`Multiple components match node with tagname ${tNode.value}`);
throw new RuntimeError(
RuntimeErrorCode.MULTIPLE_COMPONENTS_MATCH,
`Multiple components match node with tagname ${tNode.value}`);
}
export function throwMixedMultiProviderError() {
@ -57,7 +62,7 @@ export function throwErrorIfNoChangesMode(
}
// TODO: include debug context, see `viewDebugError` function in
// `packages/core/src/view/errors.ts` for reference.
throw new Error(msg);
throw new RuntimeError(RuntimeErrorCode.EXPRESSION_CHANGED_AFTER_CHECKED, msg);
}
function constructDetailsForInterpolation(
@ -121,5 +126,7 @@ export function getExpressionChangedErrorDetails(
/** Throws an error when a token is not found in DI. */
export function throwProviderNotFoundError(token: any, injectorName?: string): never {
const injectorDetails = injectorName ? ` in ${injectorName}` : '';
throw new Error(`No provider for ${stringifyForError(token)} found${injectorDetails}`);
throw new RuntimeError(
RuntimeErrorCode.PROVIDER_NOT_FOUND,
`No provider for ${stringifyForError(token)} found${injectorDetails}`);
}

View File

@ -9,6 +9,7 @@
import {assertDefined, assertEqual, assertIndexInRange} from '../../util/assert';
import {assertFirstCreatePass, assertHasParent} from '../assert';
import {attachPatchData} from '../context_discovery';
import {formatRuntimeError, RuntimeErrorCode} from '../error_code';
import {registerPostOrderHooks} from '../hooks';
import {hasClassInput, hasStyleInput, TAttributes, TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
@ -216,7 +217,7 @@ function logUnknownElementError(
message +=
`2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`;
}
console.error(message);
console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_ELEMENT, message));
}
}
}

View File

@ -21,6 +21,7 @@ import {assertFirstCreatePass, assertFirstUpdatePass, assertLContainer, assertLV
import {attachPatchData} from '../context_discovery';
import {getFactoryDef} from '../definition';
import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from '../di';
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../error_code';
import {throwMultipleComponentError} from '../errors';
import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} from '../hooks';
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS} from '../interfaces/container';
@ -1096,7 +1097,8 @@ export function matchingSchemas(tView: TView, tagName: string|null): boolean {
* @param tNode Node on which we encountered the property.
*/
function logUnknownPropertyError(propName: string, tNode: TNode): void {
console.error(`Can't bind to '${propName}' since it isn't a known property of '${tNode.value}'.`);
let message = `Can't bind to '${propName}' since it isn't a known property of '${tNode.value}'.`;
console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message));
}
/**
@ -1388,7 +1390,9 @@ function cacheMatchingLocalNames(
// in the template (for template queries).
for (let i = 0; i < localRefs.length; i += 2) {
const index = exportsMap[localRefs[i + 1]];
if (index == null) throw new Error(`Export of name '${localRefs[i + 1]}' not found!`);
if (index == null)
throw new RuntimeError(
RuntimeErrorCode.EXPORT_NOT_FOUND, `Export of name '${localRefs[i + 1]}' not found!`);
localNames.push(localRefs[i], index);
}
}

View File

@ -12,6 +12,7 @@ import {setInjectImplementation} from '../di/injector_compatibility';
import {getFactoryDef} from './definition';
import {setIncludeViewProviders} from './di';
import {RuntimeError, RuntimeErrorCode} from './error_code';
import {store, ɵɵdirectiveInject} from './instructions/all';
import {PipeDef, PipeDefList} from './interfaces/definition';
import {HEADER_OFFSET, LView, TVIEW} from './interfaces/view';
@ -80,7 +81,7 @@ function getPipeDef(name: string, registry: PipeDefList|null): PipeDef<any> {
}
}
}
throw new Error(`The pipe '${name}' could not be found!`);
throw new RuntimeError(RuntimeErrorCode.PIPE_NOT_FOUND, `The pipe '${name}' could not be found!`);
}
/**

View File

@ -616,7 +616,7 @@ describe('di', () => {
TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]});
expect(() => TestBed.createComponent(MyComp))
.toThrowError('Circular dependency in DI detected for DirectiveA');
.toThrowError('NG0200: Circular dependency in DI detected for DirectiveA');
});
onlyInIvy('Ivy has different error message for circular dependency')
@ -632,7 +632,7 @@ describe('di', () => {
TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]});
expect(() => TestBed.createComponent(MyComp))
.toThrowError('Circular dependency in DI detected for DirectiveA');
.toThrowError('NG0200: Circular dependency in DI detected for DirectiveA');
});
describe('flags', () => {
@ -736,7 +736,7 @@ describe('di', () => {
}
TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]});
expect(() => TestBed.createComponent(MyComp))
.toThrowError(/No provider for DirectiveB found in NodeInjector/);
.toThrowError(/NG0201: No provider for DirectiveB found in NodeInjector/);
});
describe('@Host', () => {
@ -814,7 +814,7 @@ describe('di', () => {
TestBed.configureTestingModule({declarations: [DirectiveString, MyComp, MyApp]});
expect(() => TestBed.createComponent(MyApp))
.toThrowError('No provider for String found in NodeInjector');
.toThrowError('NG0201: No provider for String found in NodeInjector');
});
onlyInIvy('Ivy has different error message when dependency is not found')
@ -830,7 +830,7 @@ describe('di', () => {
TestBed.configureTestingModule(
{declarations: [DirectiveA, DirectiveB, MyComp, MyApp]});
expect(() => TestBed.createComponent(MyApp))
.toThrowError(/No provider for DirectiveB found in NodeInjector/);
.toThrowError(/NG0201: No provider for DirectiveB found in NodeInjector/);
});
onlyInIvy('Ivy has different error message when dependency is not found')
@ -855,7 +855,7 @@ describe('di', () => {
expect(() => {
fixture.componentInstance.myComp.showing = true;
fixture.detectChanges();
}).toThrowError(/No provider for DirectiveB found in NodeInjector/);
}).toThrowError(/NG0201: No provider for DirectiveB found in NodeInjector/);
});
it('should find providers across embedded views if not passing component boundary', () => {
@ -894,7 +894,7 @@ describe('di', () => {
TestBed.configureTestingModule({declarations: [DirectiveComp, MyComp, MyApp]});
expect(() => TestBed.createComponent(MyApp))
.toThrowError('No provider for MyApp found in NodeInjector');
.toThrowError('NG0201: No provider for MyApp found in NodeInjector');
});
describe('regression', () => {

View File

@ -38,6 +38,9 @@
{
"name": "NodeInjectorFactory"
},
{
"name": "RuntimeError"
},
{
"name": "SimpleChange"
},

View File

@ -521,6 +521,9 @@
{
"name": "RootViewRef"
},
{
"name": "RuntimeError"
},
{
"name": "SCHEDULER"
},

View File

@ -32,6 +32,9 @@
{
"name": "NodeInjectorFactory"
},
{
"name": "RuntimeError"
},
{
"name": "SimpleChange"
},

View File

@ -671,6 +671,9 @@
{
"name": "RoutesRecognized"
},
{
"name": "RuntimeError"
},
{
"name": "SAFE_URL_PATTERN"
},

View File

@ -89,6 +89,9 @@
{
"name": "RecordViewTuple"
},
{
"name": "RuntimeError"
},
{
"name": "SWITCH_ELEMENT_REF_FACTORY"
},

View File

@ -1024,7 +1024,7 @@ function declareTests(config?: {useJit: boolean}) {
});
it('should throw when trying to instantiate a cyclic dependency', () => {
let errorMessage = ivyEnabled ? /Circular dependency in DI detected for Car/g :
let errorMessage = ivyEnabled ? /NG0200: Circular dependency in DI detected for Car/g :
/Cannot instantiate cyclic dependency! Car/g;
expect(() => createInjector([Car, {provide: Engine, useClass: CyclicEngine}]).get(Car))
.toThrowError(errorMessage);

View File

@ -628,7 +628,7 @@ describe('View injector', () => {
.it('should not instantiate a directive with cyclic dependencies', () => {
TestBed.configureTestingModule({declarations: [CycleDirective]});
expect(() => createComponent('<div cycleDirective></div>'))
.toThrowError('Circular dependency in DI detected for CycleDirective');
.toThrowError('NG0200: Circular dependency in DI detected for CycleDirective');
});
obsoleteInIvy('This error is no longer generated by the compiler')
@ -661,7 +661,7 @@ describe('View injector', () => {
SimpleComponent, {set: {template: '<div needsServiceFromHost><div>'}});
expect(() => createComponent('<div simpleComponent></div>'))
.toThrowError('No provider for service found in NodeInjector');
.toThrowError('NG0201: No provider for service found in NodeInjector');
});
obsoleteInIvy('This error is no longer generated by the compiler')
@ -694,7 +694,7 @@ describe('View injector', () => {
SimpleComponent, {set: {template: '<div needsServiceFromHost><div>'}});
expect(() => createComponent('<div simpleComponent someOtherDirective></div>'))
.toThrowError('No provider for service found in NodeInjector');
.toThrowError('NG0201: No provider for service found in NodeInjector');
});
obsoleteInIvy('This error is no longer generated by the compiler')
@ -717,7 +717,7 @@ describe('View injector', () => {
expect(
() => createComponent(
'<div simpleDirective><div needsDirectiveFromSelf></div></div>'))
.toThrowError('No provider for SimpleDirective found in NodeInjector');
.toThrowError('NG0201: No provider for SimpleDirective found in NodeInjector');
});
it('should instantiate directives that depend on other directives', fakeAsync(() => {
@ -779,7 +779,7 @@ describe('View injector', () => {
TestBed.overrideComponent(
SimpleComponent, {set: {template: '<div needsDirectiveFromHost></div>'}});
expect(() => createComponent('<div simpleComponent simpleDirective></div>'))
.toThrowError('No provider for SimpleDirective found in NodeInjector');
.toThrowError('NG0201: No provider for SimpleDirective found in NodeInjector');
});
it('should allow to use the NgModule injector from a root ViewContainerRef.parentInjector',

View File

@ -107,7 +107,7 @@ describe('di', () => {
(DirA as any)['__NG_ELEMENT_ID__'] = 1;
(DirC as any)['__NG_ELEMENT_ID__'] = 257;
new ComponentFixture(App);
}).toThrowError('No provider for DirB found in NodeInjector');
}).toThrowError('NG0201: No provider for DirB found in NodeInjector');
});
});
});