feat(ivy): improve stacktrace for R3Injector
errors (#28207)
Improve the stacktrace for `R3Injector` errors by adding the source component (or module) that tried to inject the missing provider, as well as the name of the injector which triggered the error (`R3Injector`). e.g.: ``` R3InjectorError(SomeModule)[car -> SportsCar]: NullInjectorError: No provider for SportsCar! ``` FW-807 #resolve FW-875 #resolve PR Close #28207
This commit is contained in:

committed by
Matias Niemelä

parent
7219639ff3
commit
728fe69625
@ -9,7 +9,6 @@
|
||||
import {Type} from '../interface/type';
|
||||
import {getClosureSafeProperty} from '../util/property';
|
||||
import {stringify} from '../util/stringify';
|
||||
|
||||
import {resolveForwardRef} from './forward_ref';
|
||||
import {InjectionToken} from './injection_token';
|
||||
import {inject} from './injector_compatibility';
|
||||
@ -42,7 +41,9 @@ export class NullInjector implements Injector {
|
||||
// reason why correctly written application should cause this exception.
|
||||
// TODO(misko): uncomment the next line once `ngDevMode` works with closure.
|
||||
// if(ngDevMode) debugger;
|
||||
throw new Error(`NullInjectorError: No provider for ${stringify(token)}!`);
|
||||
const error = new Error(`NullInjectorError: No provider for ${stringify(token)}!`);
|
||||
error.name = 'NullInjectorError';
|
||||
throw error;
|
||||
}
|
||||
return notFoundValue;
|
||||
}
|
||||
@ -131,7 +132,7 @@ const MULTI_PROVIDER_FN = function(): any[] {
|
||||
export const USE_VALUE =
|
||||
getClosureSafeProperty<ValueProvider>({provide: String, useValue: getClosureSafeProperty});
|
||||
const NG_TOKEN_PATH = 'ngTokenPath';
|
||||
const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath';
|
||||
export const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath';
|
||||
const enum OptionFlags {
|
||||
Optional = 1 << 0,
|
||||
CheckSelf = 1 << 1,
|
||||
@ -167,14 +168,7 @@ export class StaticInjector implements Injector {
|
||||
try {
|
||||
return tryResolveToken(token, record, this._records, this.parent, notFoundValue, flags);
|
||||
} catch (e) {
|
||||
const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH];
|
||||
if (token[SOURCE]) {
|
||||
tokenPath.unshift(token[SOURCE]);
|
||||
}
|
||||
e.message = formatError('\n' + e.message, tokenPath, this.source);
|
||||
e[NG_TOKEN_PATH] = tokenPath;
|
||||
e[NG_TEMP_TOKEN_PATH] = null;
|
||||
throw e;
|
||||
return catchInjectorError(e, token, 'StaticInjectorError', this.source);
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,8 +194,6 @@ interface DependencyRecord {
|
||||
options: number;
|
||||
}
|
||||
|
||||
type TokenPath = Array<any>;
|
||||
|
||||
function resolveProvider(provider: SupportedProvider): Record {
|
||||
const deps = computeDeps(provider);
|
||||
let fn: Function = IDENT;
|
||||
@ -385,7 +377,20 @@ function computeDeps(provider: StaticProvider): DependencyRecord[] {
|
||||
return deps;
|
||||
}
|
||||
|
||||
function formatError(text: string, obj: any, source: string | null = null): string {
|
||||
export function catchInjectorError(
|
||||
e: any, token: any, injectorErrorName: string, source: string | null): never {
|
||||
const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH];
|
||||
if (token[SOURCE]) {
|
||||
tokenPath.unshift(token[SOURCE]);
|
||||
}
|
||||
e.message = formatError('\n' + e.message, tokenPath, injectorErrorName, source);
|
||||
e[NG_TOKEN_PATH] = tokenPath;
|
||||
e[NG_TEMP_TOKEN_PATH] = null;
|
||||
throw e;
|
||||
}
|
||||
|
||||
function formatError(
|
||||
text: string, obj: any, injectorErrorName: string, source: string | null = null): string {
|
||||
text = text && text.charAt(0) === '\n' && text.charAt(1) == NO_NEW_LINE ? text.substr(2) : text;
|
||||
let context = stringify(obj);
|
||||
if (obj instanceof Array) {
|
||||
@ -401,9 +406,9 @@ function formatError(text: string, obj: any, source: string | null = null): stri
|
||||
}
|
||||
context = `{${parts.join(', ')}}`;
|
||||
}
|
||||
return `StaticInjectorError${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`;
|
||||
return `${injectorErrorName}${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`;
|
||||
}
|
||||
|
||||
function staticError(text: string, obj: any): Error {
|
||||
return new Error(formatError(text, obj));
|
||||
return new Error(formatError(text, obj, 'StaticInjectorError'));
|
||||
}
|
||||
|
@ -15,8 +15,8 @@
|
||||
export enum InjectFlags {
|
||||
// TODO(alxhub): make this 'const' when ngc no longer writes exports of it into ngfactory files.
|
||||
|
||||
/** Check self and check parent injector if needed */
|
||||
Default = 0b0000,
|
||||
|
||||
/**
|
||||
* Specifies that an injector should retrieve a dependency from any injector until reaching the
|
||||
* host element of the current component. (Only used with Element Injector)
|
||||
|
@ -9,10 +9,9 @@
|
||||
import {OnDestroy} from '../interface/lifecycle_hooks';
|
||||
import {Type} from '../interface/type';
|
||||
import {stringify} from '../util/stringify';
|
||||
|
||||
import {resolveForwardRef} from './forward_ref';
|
||||
import {InjectionToken} from './injection_token';
|
||||
import {INJECTOR, Injector, NullInjector, THROW_IF_NOT_FOUND, USE_VALUE} from './injector';
|
||||
import {INJECTOR, Injector, NG_TEMP_TOKEN_PATH, NullInjector, USE_VALUE, catchInjectorError} from './injector';
|
||||
import {inject, injectArgs, setCurrentInjector} from './injector_compatibility';
|
||||
import {InjectableDef, InjectableType, InjectorType, InjectorTypeWithProviders, getInjectableDef, getInjectorDef} from './interface/defs';
|
||||
import {InjectFlags} from './interface/injector';
|
||||
@ -20,7 +19,6 @@ import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, S
|
||||
import {APP_ROOT} from './scope';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Internal type for a single provider in a deep provider array.
|
||||
*/
|
||||
@ -72,9 +70,9 @@ interface Record<T> {
|
||||
*/
|
||||
export function createInjector(
|
||||
defType: /* InjectorType<any> */ any, parent: Injector | null = null,
|
||||
additionalProviders: StaticProvider[] | null = null): Injector {
|
||||
additionalProviders: StaticProvider[] | null = null, name?: string): Injector {
|
||||
parent = parent || getNullInjector();
|
||||
return new R3Injector(defType, additionalProviders, parent);
|
||||
return new R3Injector(defType, additionalProviders, parent, name);
|
||||
}
|
||||
|
||||
export class R3Injector {
|
||||
@ -99,6 +97,8 @@ export class R3Injector {
|
||||
*/
|
||||
private readonly isRootInjector: boolean;
|
||||
|
||||
readonly source: string|null;
|
||||
|
||||
/**
|
||||
* Flag indicating that this injector was previously destroyed.
|
||||
*/
|
||||
@ -106,8 +106,8 @@ export class R3Injector {
|
||||
private _destroyed = false;
|
||||
|
||||
constructor(
|
||||
def: InjectorType<any>, additionalProviders: StaticProvider[]|null,
|
||||
readonly parent: Injector) {
|
||||
def: InjectorType<any>, additionalProviders: StaticProvider[]|null, readonly parent: Injector,
|
||||
source: string|null = null) {
|
||||
// Start off by creating Records for every provider declared in every InjectorType
|
||||
// included transitively in `def`.
|
||||
const dedupStack: InjectorType<any>[] = [];
|
||||
@ -127,6 +127,9 @@ export class R3Injector {
|
||||
|
||||
// Eagerly instantiate the InjectorType classes themselves.
|
||||
this.injectorDefTypes.forEach(defType => this.get(defType));
|
||||
|
||||
// Source name, used for debugging
|
||||
this.source = source || (def instanceof Array ? null : stringify(def));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,7 +155,7 @@ export class R3Injector {
|
||||
}
|
||||
|
||||
get<T>(
|
||||
token: Type<T>|InjectionToken<T>, notFoundValue: any = THROW_IF_NOT_FOUND,
|
||||
token: Type<T>|InjectionToken<T>, notFoundValue: any = Injector.THROW_IF_NOT_FOUND,
|
||||
flags = InjectFlags.Default): T {
|
||||
this.assertNotDestroyed();
|
||||
// Set the injection context.
|
||||
@ -182,7 +185,21 @@ export class R3Injector {
|
||||
// Select the next injector based on the Self flag - if self is set, the next injector is
|
||||
// the NullInjector, otherwise it's the parent.
|
||||
const nextInjector = !(flags & InjectFlags.Self) ? this.parent : getNullInjector();
|
||||
return nextInjector.get(token, notFoundValue);
|
||||
return nextInjector.get(token, flags & InjectFlags.Optional ? null : notFoundValue);
|
||||
} catch (e) {
|
||||
if (e.name === 'NullInjectorError') {
|
||||
const path: any[] = e[NG_TEMP_TOKEN_PATH] = e[NG_TEMP_TOKEN_PATH] || [];
|
||||
path.unshift(stringify(token));
|
||||
if (previousInjector) {
|
||||
// We still have a parent injector, keep throwing
|
||||
throw e;
|
||||
} else {
|
||||
// Format & throw the final error message when we don't have any previous injector
|
||||
return catchInjectorError(e, token, 'R3InjectorError', this.source);
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
// Lastly, clean up the state by restoring the previous injector.
|
||||
setCurrentInjector(previousInjector);
|
||||
|
@ -16,7 +16,6 @@ import {InternalNgModuleRef, NgModuleFactory as viewEngine_NgModuleFactory, NgMo
|
||||
import {NgModuleDef} from '../metadata/ng_module';
|
||||
import {assertDefined} from '../util/assert';
|
||||
import {stringify} from '../util/stringify';
|
||||
|
||||
import {ComponentFactoryResolver} from './component_ref';
|
||||
import {getNgModuleDef} from './definition';
|
||||
|
||||
@ -52,7 +51,8 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
|
||||
},
|
||||
COMPONENT_FACTORY_RESOLVER
|
||||
];
|
||||
this._r3Injector = createInjector(ngModuleType, _parent, additionalProviders) as R3Injector;
|
||||
this._r3Injector = createInjector(
|
||||
ngModuleType, _parent, additionalProviders, stringify(ngModuleType)) as R3Injector;
|
||||
this.instance = this.get(ngModuleType);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user