feat(core): add source to `StaticInjectorError` message (#20817)

Closes #19302
PR Close #20817
This commit is contained in:
Olivier Combe 2017-12-06 10:13:50 +01:00 committed by Jason Aden
parent 634d33f5dd
commit b7738e1fe5
11 changed files with 106 additions and 34 deletions

View File

@ -102,7 +102,8 @@ export function createPlatformFactory(
parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef) | null, parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef) | null,
name: string, providers: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => name: string, providers: StaticProvider[] = []): (extraProviders?: StaticProvider[]) =>
PlatformRef { PlatformRef {
const marker = new InjectionToken(`Platform: ${name}`); const desc = `Platform: ${name}`;
const marker = new InjectionToken(desc);
return (extraProviders: StaticProvider[] = []) => { return (extraProviders: StaticProvider[] = []) => {
let platform = getPlatform(); let platform = getPlatform();
if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) {
@ -110,8 +111,9 @@ export function createPlatformFactory(
parentPlatformFactory( parentPlatformFactory(
providers.concat(extraProviders).concat({provide: marker, useValue: true})); providers.concat(extraProviders).concat({provide: marker, useValue: true}));
} else { } else {
createPlatform(Injector.create( const injectedProviders: StaticProvider[] =
providers.concat(extraProviders).concat({provide: marker, useValue: true}))); providers.concat(extraProviders).concat({provide: marker, useValue: true});
createPlatform(Injector.create({providers: injectedProviders, name: desc}));
} }
} }
return assertPlatform(marker); return assertPlatform(marker);
@ -224,10 +226,12 @@ export class PlatformRef {
// pass that as parent to the NgModuleFactory. // pass that as parent to the NgModuleFactory.
const ngZoneOption = options ? options.ngZone : undefined; const ngZoneOption = options ? options.ngZone : undefined;
const ngZone = getNgZone(ngZoneOption); const ngZone = getNgZone(ngZoneOption);
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
// Attention: Don't use ApplicationRef.run here, // Attention: Don't use ApplicationRef.run here,
// as we want to be sure that all possible constructor calls are inside `ngZone.run`! // as we want to be sure that all possible constructor calls are inside `ngZone.run`!
return ngZone.run(() => { return ngZone.run(() => {
const ngZoneInjector = Injector.create([{provide: NgZone, useValue: ngZone}], this.injector); const ngZoneInjector = Injector.create(
{providers: providers, parent: this.injector, name: moduleFactory.moduleType.name});
const moduleRef = <InternalNgModuleRef<M>>moduleFactory.create(ngZoneInjector); const moduleRef = <InternalNgModuleRef<M>>moduleFactory.create(ngZoneInjector);
const exceptionHandler: ErrorHandler = moduleRef.injector.get(ErrorHandler, null); const exceptionHandler: ErrorHandler = moduleRef.injector.get(ErrorHandler, null);
if (!exceptionHandler) { if (!exceptionHandler) {

View File

@ -8,12 +8,12 @@
import {Type} from '../type'; import {Type} from '../type';
import {stringify} from '../util'; import {stringify} from '../util';
import {resolveForwardRef} from './forward_ref'; import {resolveForwardRef} from './forward_ref';
import {InjectionToken} from './injection_token'; import {InjectionToken} from './injection_token';
import {Inject, Optional, Self, SkipSelf} from './metadata'; import {Inject, Optional, Self, SkipSelf} from './metadata';
import {ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, ValueProvider} from './provider'; import {ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, ValueProvider} from './provider';
export const SOURCE = '__source';
const _THROW_IF_NOT_FOUND = new Object(); const _THROW_IF_NOT_FOUND = new Object();
export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND; export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND;
@ -64,6 +64,13 @@ export abstract class Injector {
*/ */
abstract get(token: any, notFoundValue?: any): any; abstract get(token: any, notFoundValue?: any): any;
/**
* @deprecated from v5 use the new signature Injector.create(options)
*/
static create(providers: StaticProvider[], parent?: Injector): Injector;
static create(options: {providers: StaticProvider[], parent?: Injector, name?: string}): Injector;
/** /**
* Create a new Injector which is configure using `StaticProvider`s. * Create a new Injector which is configure using `StaticProvider`s.
* *
@ -71,8 +78,14 @@ export abstract class Injector {
* *
* {@example core/di/ts/provider_spec.ts region='ConstructorProvider'} * {@example core/di/ts/provider_spec.ts region='ConstructorProvider'}
*/ */
static create(providers: StaticProvider[], parent?: Injector): Injector { static create(
return new StaticInjector(providers, parent); options: StaticProvider[]|{providers: StaticProvider[], parent?: Injector, name?: string},
parent?: Injector): Injector {
if (Array.isArray(options)) {
return new StaticInjector(options, parent);
} else {
return new StaticInjector(options.providers, options.parent, options.name || null);
}
} }
} }
@ -103,11 +116,14 @@ const NO_NEW_LINE = 'ɵ';
export class StaticInjector implements Injector { export class StaticInjector implements Injector {
readonly parent: Injector; readonly parent: Injector;
readonly source: string|null;
private _records: Map<any, Record>; private _records: Map<any, Record>;
constructor(providers: StaticProvider[], parent: Injector = NULL_INJECTOR) { constructor(
providers: StaticProvider[], parent: Injector = NULL_INJECTOR, source: string|null = null) {
this.parent = parent; this.parent = parent;
this.source = source;
const records = this._records = new Map<any, Record>(); const records = this._records = new Map<any, Record>();
records.set( records.set(
Injector, <Record>{token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false}); Injector, <Record>{token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false});
@ -122,7 +138,10 @@ export class StaticInjector implements Injector {
return tryResolveToken(token, record, this._records, this.parent, notFoundValue); return tryResolveToken(token, record, this._records, this.parent, notFoundValue);
} catch (e) { } catch (e) {
const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH]; const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH];
e.message = formatError('\n' + e.message, tokenPath); if (token[SOURCE]) {
tokenPath.unshift(token[SOURCE]);
}
e.message = formatError('\n' + e.message, tokenPath, this.source);
e[NG_TOKEN_PATH] = tokenPath; e[NG_TOKEN_PATH] = tokenPath;
e[NG_TEMP_TOKEN_PATH] = null; e[NG_TEMP_TOKEN_PATH] = null;
throw e; throw e;
@ -336,7 +355,7 @@ function computeDeps(provider: StaticProvider): DependencyRecord[] {
return deps; return deps;
} }
function formatError(text: string, obj: any): string { function formatError(text: string, obj: any, source: string | null = null): string {
text = text && text.charAt(0) === '\n' && text.charAt(1) == NO_NEW_LINE ? text.substr(2) : text; text = text && text.charAt(0) === '\n' && text.charAt(1) == NO_NEW_LINE ? text.substr(2) : text;
let context = stringify(obj); let context = stringify(obj);
if (obj instanceof Array) { if (obj instanceof Array) {
@ -352,7 +371,7 @@ function formatError(text: string, obj: any): string {
} }
context = `{${parts.join(', ')}}`; context = `{${parts.join(', ')}}`;
} }
return `StaticInjectorError[${context}]: ${text.replace(NEW_LINE, '\n ')}`; return `StaticInjectorError${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`;
} }
function staticError(text: string, obj: any): Error { function staticError(text: string, obj: any): Error {

View File

@ -9,6 +9,7 @@
import {resolveForwardRef} from '../di/forward_ref'; import {resolveForwardRef} from '../di/forward_ref';
import {Injector} from '../di/injector'; import {Injector} from '../di/injector';
import {NgModuleRef} from '../linker/ng_module_factory'; import {NgModuleRef} from '../linker/ng_module_factory';
import {stringify} from '../util';
import {DepDef, DepFlags, NgModuleData, NgModuleDefinition, NgModuleProviderDef, NodeFlags} from './types'; import {DepDef, DepFlags, NgModuleData, NgModuleDefinition, NgModuleProviderDef, NodeFlags} from './types';
import {splitDepsDsl, tokenKey} from './util'; import {splitDepsDsl, tokenKey} from './util';
@ -25,7 +26,7 @@ export function moduleProvideDef(
// lowered the expression and then stopped evaluating it, // lowered the expression and then stopped evaluating it,
// i.e. also didn't unwrap it. // i.e. also didn't unwrap it.
value = resolveForwardRef(value); value = resolveForwardRef(value);
const depDefs = splitDepsDsl(deps); const depDefs = splitDepsDsl(deps, stringify(token));
return { return {
// will bet set by the module definition // will bet set by the module definition
index: -1, index: -1,

View File

@ -12,7 +12,7 @@ import {ElementRef} from '../linker/element_ref';
import {TemplateRef} from '../linker/template_ref'; import {TemplateRef} from '../linker/template_ref';
import {ViewContainerRef} from '../linker/view_container_ref'; import {ViewContainerRef} from '../linker/view_container_ref';
import {Renderer as RendererV1, Renderer2} from '../render/api'; import {Renderer as RendererV1, Renderer2} from '../render/api';
import {stringify} from '../util';
import {createChangeDetectorRef, createInjector, createRendererV1} from './refs'; import {createChangeDetectorRef, createInjector, createRendererV1} from './refs';
import {BindingDef, BindingFlags, DepDef, DepFlags, NodeDef, NodeFlags, OutputDef, OutputType, ProviderData, QueryValueType, Services, ViewData, ViewFlags, ViewState, asElementData, asProviderData, shouldCallLifecycleInitHook} from './types'; import {BindingDef, BindingFlags, DepDef, DepFlags, NodeDef, NodeFlags, OutputDef, OutputType, ProviderData, QueryValueType, Services, ViewData, ViewFlags, ViewState, asElementData, asProviderData, shouldCallLifecycleInitHook} from './types';
import {calcBindingFlags, checkBinding, dispatchEvent, isComponentView, splitDepsDsl, splitMatchedQueriesDsl, tokenKey, viewParentEl} from './util'; import {calcBindingFlags, checkBinding, dispatchEvent, isComponentView, splitDepsDsl, splitMatchedQueriesDsl, tokenKey, viewParentEl} from './util';
@ -83,7 +83,7 @@ export function _def(
// i.e. also didn't unwrap it. // i.e. also didn't unwrap it.
value = resolveForwardRef(value); value = resolveForwardRef(value);
const depDefs = splitDepsDsl(deps); const depDefs = splitDepsDsl(deps, stringify(token));
return { return {
// will bet set by the view definition // will bet set by the view definition

View File

@ -7,10 +7,10 @@
*/ */
import {WrappedValue, devModeEqual} from '../change_detection/change_detection'; import {WrappedValue, devModeEqual} from '../change_detection/change_detection';
import {SOURCE} from '../di/injector';
import {ViewEncapsulation} from '../metadata/view'; import {ViewEncapsulation} from '../metadata/view';
import {RendererType2} from '../render/api'; import {RendererType2} from '../render/api';
import {looseIdentical, stringify} from '../util'; import {looseIdentical, stringify} from '../util';
import {expressionChangedAfterItHasBeenCheckedError} from './errors'; import {expressionChangedAfterItHasBeenCheckedError} from './errors';
import {BindingDef, BindingFlags, Definition, DefinitionFactory, DepDef, DepFlags, ElementData, NodeDef, NodeFlags, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asTextData} from './types'; import {BindingDef, BindingFlags, Definition, DefinitionFactory, DepDef, DepFlags, ElementData, NodeDef, NodeFlags, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asTextData} from './types';
@ -209,7 +209,7 @@ export function splitMatchedQueriesDsl(
return {matchedQueries, references, matchedQueryIds}; return {matchedQueries, references, matchedQueryIds};
} }
export function splitDepsDsl(deps: ([DepFlags, any] | any)[]): DepDef[] { export function splitDepsDsl(deps: ([DepFlags, any] | any)[], sourceName?: string): DepDef[] {
return deps.map(value => { return deps.map(value => {
let token: any; let token: any;
let flags: DepFlags; let flags: DepFlags;
@ -219,6 +219,9 @@ export function splitDepsDsl(deps: ([DepFlags, any] | any)[]): DepDef[] {
flags = DepFlags.None; flags = DepFlags.None;
token = value; token = value;
} }
if (token && (typeof token === 'function' || typeof token === 'object') && sourceName) {
Object.defineProperty(token, SOURCE, {value: sourceName, configurable: true});
}
return {flags, token, tokenKey: tokenKey(token)}; return {flags, token, tokenKey: tokenKey(token)};
}); });
} }

View File

@ -147,8 +147,8 @@ export function main() {
expect(() => createAndGetRootNodes(compViewDef(rootElNodes))) expect(() => createAndGetRootNodes(compViewDef(rootElNodes)))
.toThrowError( .toThrowError(
'StaticInjectorError[Dep]: \n' + 'StaticInjectorError(DynamicTestModule)[SomeService -> Dep]: \n' +
' StaticInjectorError[Dep]: \n' + ' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' +
' NullInjectorError: No provider for Dep!'); ' NullInjectorError: No provider for Dep!');
const nonRootElNodes = [ const nonRootElNodes = [
@ -161,8 +161,8 @@ export function main() {
expect(() => createAndGetRootNodes(compViewDef(nonRootElNodes))) expect(() => createAndGetRootNodes(compViewDef(nonRootElNodes)))
.toThrowError( .toThrowError(
'StaticInjectorError[Dep]: \n' + 'StaticInjectorError(DynamicTestModule)[SomeService -> Dep]: \n' +
' StaticInjectorError[Dep]: \n' + ' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' +
' NullInjectorError: No provider for Dep!'); ' NullInjectorError: No provider for Dep!');
}); });
@ -186,8 +186,8 @@ export function main() {
directiveDef(1, NodeFlags.None, null, 0, SomeService, ['nonExistingDep']) directiveDef(1, NodeFlags.None, null, 0, SomeService, ['nonExistingDep'])
]))) ])))
.toThrowError( .toThrowError(
'StaticInjectorError[nonExistingDep]: \n' + 'StaticInjectorError(DynamicTestModule)[nonExistingDep]: \n' +
' StaticInjectorError[nonExistingDep]: \n' + ' StaticInjectorError(Platform: core)[nonExistingDep]: \n' +
' NullInjectorError: No provider for nonExistingDep!'); ' NullInjectorError: No provider for nonExistingDep!');
}); });

View File

@ -355,8 +355,12 @@ export class TestBed implements Injector {
} }
const ngZone = new NgZone({enableLongStackTrace: true}); const ngZone = new NgZone({enableLongStackTrace: true});
const ngZoneInjector = const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
Injector.create([{provide: NgZone, useValue: ngZone}], this.platform.injector); const ngZoneInjector = Injector.create({
providers: providers,
parent: this.platform.injector,
name: this._moduleFactory.moduleType.name
});
this._moduleRef = this._moduleFactory.create(ngZoneInjector); this._moduleRef = this._moduleFactory.create(ngZoneInjector);
// ApplicationInitStatus.runInitializers() is marked @internal to core. So casting to any // ApplicationInitStatus.runInitializers() is marked @internal to core. So casting to any
// before accessing it. // before accessing it.

View File

@ -135,7 +135,7 @@ export function main() {
name = 'square'; name = 'square';
} }
const injector = Injector.create([{provide: Square, deps: []}]); const injector = Injector.create({providers: [{provide: Square, deps: []}]});
const shape: Square = injector.get(Square); const shape: Square = injector.get(Square);
expect(shape.name).toEqual('square'); expect(shape.name).toEqual('square');

View File

@ -7,12 +7,12 @@
*/ */
import {isPlatformBrowser} from '@angular/common'; import {isPlatformBrowser} from '@angular/common';
import {APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA, Compiler, Component, Directive, ErrorHandler, Inject, Input, LOCALE_ID, NgModule, OnDestroy, PLATFORM_ID, PLATFORM_INITIALIZER, Pipe, Provider, StaticProvider, VERSION, createPlatformFactory, ɵstringify as stringify} from '@angular/core'; import {APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA, Compiler, Component, Directive, ErrorHandler, Inject, Input, LOCALE_ID, NgModule, OnDestroy, PLATFORM_ID, PLATFORM_INITIALIZER, Pipe, Provider, StaticProvider, Type, VERSION, createPlatformFactory} from '@angular/core';
import {ApplicationRef, destroyPlatform} from '@angular/core/src/application_ref'; import {ApplicationRef, destroyPlatform} from '@angular/core/src/application_ref';
import {Console} from '@angular/core/src/console'; import {Console} from '@angular/core/src/console';
import {ComponentRef} from '@angular/core/src/linker/component_factory'; import {ComponentRef} from '@angular/core/src/linker/component_factory';
import {Testability, TestabilityRegistry} from '@angular/core/src/testability/testability'; import {Testability, TestabilityRegistry} from '@angular/core/src/testability/testability';
import {AsyncTestCompleter, Log, afterEach, beforeEach, beforeEachProviders, ddescribe, describe, iit, inject, it} from '@angular/core/testing/src/testing_internal'; import {AsyncTestCompleter, Log, afterEach, beforeEach, beforeEachProviders, describe, iit, inject, it} from '@angular/core/testing/src/testing_internal';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -112,10 +112,11 @@ class DummyConsole implements Console {
class TestModule {} class TestModule {}
function bootstrap(cmpType: any, providers: Provider[] = [], platformProviders: StaticProvider[] = [ function bootstrap(
]): Promise<any> { cmpType: any, providers: Provider[] = [], platformProviders: StaticProvider[] = [],
imports: Type<any>[] = []): Promise<any> {
@NgModule({ @NgModule({
imports: [BrowserModule], imports: [BrowserModule, ...imports],
declarations: [cmpType], declarations: [cmpType],
bootstrap: [cmpType], bootstrap: [cmpType],
providers: providers, providers: providers,
@ -183,6 +184,40 @@ export function main() {
}); });
})); }));
it('should throw if no provider', inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const logger = new MockConsole();
const errorHandler = new ErrorHandler();
errorHandler._console = logger as any;
class IDontExist {}
@Component({selector: 'cmp', template: 'Cmp'})
class CustomCmp {
constructor(iDontExist: IDontExist) {}
}
@Component({
selector: 'hello-app',
template: '<cmp></cmp>',
})
class RootCmp {
}
@NgModule({declarations: [CustomCmp], exports: [CustomCmp]})
class CustomModule {
}
bootstrap(RootCmp, [{provide: ErrorHandler, useValue: errorHandler}], [], [
CustomModule
]).then(null, (e: Error) => {
expect(e.message).toContain(`StaticInjectorError(TestModule)[CustomCmp -> IDontExist]:
StaticInjectorError(Platform: core)[CustomCmp -> IDontExist]:
NullInjectorError: No provider for IDontExist!`);
async.done();
return null;
});
}));
if (getDOM().supportsDOMEvents()) { if (getDOM().supportsDOMEvents()) {
it('should forward the error to promise when bootstrap fails', it('should forward the error to promise when bootstrap fails',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Testability, TestabilityRegistry, Type} from '@angular/core'; import {ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, StaticProvider, Testability, TestabilityRegistry, Type} from '@angular/core';
import * as angular from './angular1'; import * as angular from './angular1';
import {PropertyBinding} from './component_info'; import {PropertyBinding} from './component_info';
@ -54,8 +54,9 @@ export class DowngradeComponentAdapter {
} }
createComponent(projectableNodes: Node[][]) { createComponent(projectableNodes: Node[][]) {
const childInjector = const providers: StaticProvider[] = [{provide: $SCOPE, useValue: this.componentScope}];
Injector.create([{provide: $SCOPE, useValue: this.componentScope}], this.parentInjector); const childInjector = Injector.create(
{providers: providers, parent: this.parentInjector, name: 'DowngradeComponentAdapter'});
this.componentRef = this.componentRef =
this.componentFactory.create(childInjector, projectableNodes, this.element[0]); this.componentFactory.create(childInjector, projectableNodes, this.element[0]);

View File

@ -476,7 +476,12 @@ export declare abstract class Injector {
/** @deprecated */ abstract get(token: any, notFoundValue?: any): any; /** @deprecated */ abstract get(token: any, notFoundValue?: any): any;
static NULL: Injector; static NULL: Injector;
static THROW_IF_NOT_FOUND: Object; static THROW_IF_NOT_FOUND: Object;
static create(providers: StaticProvider[], parent?: Injector): Injector; /** @deprecated */ static create(providers: StaticProvider[], parent?: Injector): Injector;
static create(options: {
providers: StaticProvider[];
parent?: Injector;
name?: string;
}): Injector;
} }
/** @stable */ /** @stable */