From e6ca3d3841bc044eb30e6b891d459e3002da6d88 Mon Sep 17 00:00:00 2001 From: twerske Date: Mon, 26 Oct 2020 15:45:31 -0700 Subject: [PATCH] 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 --- .../guide/dependency-injection-navtree.md | 2 +- goldens/size-tracking/aio-payloads.json | 2 +- .../ivy_build/app/test/module_spec.ts | 6 ++- packages/core/src/render3/error_code.ts | 45 +++++++++++++++++++ packages/core/src/render3/errors.ts | 15 +++++-- .../core/src/render3/instructions/element.ts | 3 +- .../core/src/render3/instructions/shared.ts | 8 +++- packages/core/src/render3/pipe.ts | 3 +- packages/core/test/acceptance/di_spec.ts | 14 +++--- .../cyclic_import/bundle.golden_symbols.json | 3 ++ .../bundling/forms/bundle.golden_symbols.json | 3 ++ .../hello_world/bundle.golden_symbols.json | 3 ++ .../router/bundle.golden_symbols.json | 3 ++ .../bundling/todo/bundle.golden_symbols.json | 3 ++ .../test/linker/ng_module_integration_spec.ts | 2 +- .../linker/view_injector_integration_spec.ts | 10 ++--- packages/core/test/render3/di_spec.ts | 2 +- 17 files changed, 101 insertions(+), 26 deletions(-) create mode 100644 packages/core/src/render3/error_code.ts diff --git a/aio/content/guide/dependency-injection-navtree.md b/aio/content/guide/dependency-injection-navtree.md index 3eff1da2c2..7bac02dd8c 100644 --- a/aio/content/guide/dependency-injection-navtree.md +++ b/aio/content/guide/dependency-injection-navtree.md @@ -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. diff --git a/goldens/size-tracking/aio-payloads.json b/goldens/size-tracking/aio-payloads.json index 0e7403b502..3cefa0c90f 100755 --- a/goldens/size-tracking/aio-payloads.json +++ b/goldens/size-tracking/aio-payloads.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 3037, - "main-es2015": 448085, + "main-es2015": 448615, "polyfills-es2015": 52415 } } diff --git a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts index c9dfbe4a92..24ff406205 100644 --- a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts +++ b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts @@ -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', () => { diff --git a/packages/core/src/render3/error_code.ts b/packages/core/src/render3/error_code.ts new file mode 100644 index 0000000000..b68d111b07 --- /dev/null +++ b/packages/core/src/render3/error_code.ts @@ -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}`; +} diff --git a/packages/core/src/render3/errors.ts b/packages/core/src/render3/errors.ts index 2ffd70fd35..01c66935ca 100644 --- a/packages/core/src/render3/errors.ts +++ b/packages/core/src/render3/errors.ts @@ -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}`); } diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index b03b4fdbf8..8c2fe67703 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -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)); } } } diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 024dda0334..294dc3e977 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -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); } } diff --git a/packages/core/src/render3/pipe.ts b/packages/core/src/render3/pipe.ts index 0d33ad41f1..e9fb78a1ae 100644 --- a/packages/core/src/render3/pipe.ts +++ b/packages/core/src/render3/pipe.ts @@ -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 { } } } - throw new Error(`The pipe '${name}' could not be found!`); + throw new RuntimeError(RuntimeErrorCode.PIPE_NOT_FOUND, `The pipe '${name}' could not be found!`); } /** diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index 8bc0206a56..768fbf5827 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -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', () => { diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index 0d1b098faa..cc440fb4aa 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -38,6 +38,9 @@ { "name": "NodeInjectorFactory" }, + { + "name": "RuntimeError" + }, { "name": "SimpleChange" }, diff --git a/packages/core/test/bundling/forms/bundle.golden_symbols.json b/packages/core/test/bundling/forms/bundle.golden_symbols.json index 4333990670..829ac9a090 100644 --- a/packages/core/test/bundling/forms/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms/bundle.golden_symbols.json @@ -521,6 +521,9 @@ { "name": "RootViewRef" }, + { + "name": "RuntimeError" + }, { "name": "SCHEDULER" }, diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 93d584fe53..dd2c07f81c 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -32,6 +32,9 @@ { "name": "NodeInjectorFactory" }, + { + "name": "RuntimeError" + }, { "name": "SimpleChange" }, diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index ab1de47fc9..3757fd55ed 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -671,6 +671,9 @@ { "name": "RoutesRecognized" }, + { + "name": "RuntimeError" + }, { "name": "SAFE_URL_PATTERN" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 55f744fd90..197a374cf8 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -89,6 +89,9 @@ { "name": "RecordViewTuple" }, + { + "name": "RuntimeError" + }, { "name": "SWITCH_ELEMENT_REF_FACTORY" }, diff --git a/packages/core/test/linker/ng_module_integration_spec.ts b/packages/core/test/linker/ng_module_integration_spec.ts index d57678cd26..d3dc195b99 100644 --- a/packages/core/test/linker/ng_module_integration_spec.ts +++ b/packages/core/test/linker/ng_module_integration_spec.ts @@ -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); diff --git a/packages/core/test/linker/view_injector_integration_spec.ts b/packages/core/test/linker/view_injector_integration_spec.ts index cee9d4ec50..ecb9b2ab5b 100644 --- a/packages/core/test/linker/view_injector_integration_spec.ts +++ b/packages/core/test/linker/view_injector_integration_spec.ts @@ -628,7 +628,7 @@ describe('View injector', () => { .it('should not instantiate a directive with cyclic dependencies', () => { TestBed.configureTestingModule({declarations: [CycleDirective]}); expect(() => createComponent('
')) - .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: '
'}}); expect(() => createComponent('
')) - .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: '
'}}); expect(() => createComponent('
')) - .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( '
')) - .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: '
'}}); expect(() => createComponent('
')) - .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', diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 768917188c..32aab952a7 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -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'); }); }); });