Skip to content

Commit

Permalink
feat(core): add source to StaticInjectorError message (#20817)
Browse files Browse the repository at this point in the history
Closes #19302
PR Close #20817
  • Loading branch information
ocombe authored and jasonaden committed Dec 12, 2017
1 parent 634d33f commit b7738e1
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 34 deletions.
12 changes: 8 additions & 4 deletions packages/core/src/application_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,18 @@ export function createPlatformFactory(
parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef) | null,
name: string, providers: StaticProvider[] = []): (extraProviders?: StaticProvider[]) =>
PlatformRef {
const marker = new InjectionToken(`Platform: ${name}`);
const desc = `Platform: ${name}`;
const marker = new InjectionToken(desc);
return (extraProviders: StaticProvider[] = []) => {
let platform = getPlatform();
if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) {
if (parentPlatformFactory) {
parentPlatformFactory(
providers.concat(extraProviders).concat({provide: marker, useValue: true}));
} else {
createPlatform(Injector.create(
providers.concat(extraProviders).concat({provide: marker, useValue: true})));
const injectedProviders: StaticProvider[] =
providers.concat(extraProviders).concat({provide: marker, useValue: true});
createPlatform(Injector.create({providers: injectedProviders, name: desc}));
}
}
return assertPlatform(marker);
Expand Down Expand Up @@ -224,10 +226,12 @@ export class PlatformRef {
// pass that as parent to the NgModuleFactory.
const ngZoneOption = options ? options.ngZone : undefined;
const ngZone = getNgZone(ngZoneOption);
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
// Attention: Don't use ApplicationRef.run here,
// as we want to be sure that all possible constructor calls are inside `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 exceptionHandler: ErrorHandler = moduleRef.injector.get(ErrorHandler, null);
if (!exceptionHandler) {
Expand Down
33 changes: 26 additions & 7 deletions packages/core/src/di/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

import {Type} from '../type';
import {stringify} from '../util';

import {resolveForwardRef} from './forward_ref';
import {InjectionToken} from './injection_token';
import {Inject, Optional, Self, SkipSelf} from './metadata';
import {ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, ValueProvider} from './provider';

export const SOURCE = '__source';
const _THROW_IF_NOT_FOUND = new Object();
export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND;

Expand Down Expand Up @@ -64,15 +64,28 @@ export abstract class Injector {
*/
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.
*
* ### Example
*
* {@example core/di/ts/provider_spec.ts region='ConstructorProvider'}
*/
static create(providers: StaticProvider[], parent?: Injector): Injector {
return new StaticInjector(providers, parent);
static create(
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);
}
}
}

Expand Down Expand Up @@ -103,11 +116,14 @@ const NO_NEW_LINE = 'ɵ';

export class StaticInjector implements Injector {
readonly parent: Injector;
readonly source: string|null;

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.source = source;
const records = this._records = new Map<any, Record>();
records.set(
Injector, <Record>{token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false});
Expand All @@ -122,7 +138,10 @@ export class StaticInjector implements Injector {
return tryResolveToken(token, record, this._records, this.parent, notFoundValue);
} catch (e) {
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_TEMP_TOKEN_PATH] = null;
throw e;
Expand Down Expand Up @@ -336,7 +355,7 @@ function computeDeps(provider: StaticProvider): DependencyRecord[] {
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;
let context = stringify(obj);
if (obj instanceof Array) {
Expand All @@ -352,7 +371,7 @@ function formatError(text: string, obj: any): string {
}
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 {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/view/ng_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {resolveForwardRef} from '../di/forward_ref';
import {Injector} from '../di/injector';
import {NgModuleRef} from '../linker/ng_module_factory';
import {stringify} from '../util';

import {DepDef, DepFlags, NgModuleData, NgModuleDefinition, NgModuleProviderDef, NodeFlags} from './types';
import {splitDepsDsl, tokenKey} from './util';
Expand All @@ -25,7 +26,7 @@ export function moduleProvideDef(
// lowered the expression and then stopped evaluating it,
// i.e. also didn't unwrap it.
value = resolveForwardRef(value);
const depDefs = splitDepsDsl(deps);
const depDefs = splitDepsDsl(deps, stringify(token));
return {
// will bet set by the module definition
index: -1,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/view/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {ElementRef} from '../linker/element_ref';
import {TemplateRef} from '../linker/template_ref';
import {ViewContainerRef} from '../linker/view_container_ref';
import {Renderer as RendererV1, Renderer2} from '../render/api';

import {stringify} from '../util';
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 {calcBindingFlags, checkBinding, dispatchEvent, isComponentView, splitDepsDsl, splitMatchedQueriesDsl, tokenKey, viewParentEl} from './util';
Expand Down Expand Up @@ -83,7 +83,7 @@ export function _def(
// i.e. also didn't unwrap it.
value = resolveForwardRef(value);

const depDefs = splitDepsDsl(deps);
const depDefs = splitDepsDsl(deps, stringify(token));

return {
// will bet set by the view definition
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/view/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
*/

import {WrappedValue, devModeEqual} from '../change_detection/change_detection';
import {SOURCE} from '../di/injector';
import {ViewEncapsulation} from '../metadata/view';
import {RendererType2} from '../render/api';
import {looseIdentical, stringify} from '../util';

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';

Expand Down Expand Up @@ -209,7 +209,7 @@ export function splitMatchedQueriesDsl(
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 => {
let token: any;
let flags: DepFlags;
Expand All @@ -219,6 +219,9 @@ export function splitDepsDsl(deps: ([DepFlags, any] | any)[]): DepDef[] {
flags = DepFlags.None;
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)};
});
}
Expand Down
12 changes: 6 additions & 6 deletions packages/core/test/view/provider_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ export function main() {

expect(() => createAndGetRootNodes(compViewDef(rootElNodes)))
.toThrowError(
'StaticInjectorError[Dep]: \n' +
' StaticInjectorError[Dep]: \n' +
'StaticInjectorError(DynamicTestModule)[SomeService -> Dep]: \n' +
' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' +
' NullInjectorError: No provider for Dep!');

const nonRootElNodes = [
Expand All @@ -161,8 +161,8 @@ export function main() {

expect(() => createAndGetRootNodes(compViewDef(nonRootElNodes)))
.toThrowError(
'StaticInjectorError[Dep]: \n' +
' StaticInjectorError[Dep]: \n' +
'StaticInjectorError(DynamicTestModule)[SomeService -> Dep]: \n' +
' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' +
' NullInjectorError: No provider for Dep!');
});

Expand All @@ -186,8 +186,8 @@ export function main() {
directiveDef(1, NodeFlags.None, null, 0, SomeService, ['nonExistingDep'])
])))
.toThrowError(
'StaticInjectorError[nonExistingDep]: \n' +
' StaticInjectorError[nonExistingDep]: \n' +
'StaticInjectorError(DynamicTestModule)[nonExistingDep]: \n' +
' StaticInjectorError(Platform: core)[nonExistingDep]: \n' +
' NullInjectorError: No provider for nonExistingDep!');
});

Expand Down
8 changes: 6 additions & 2 deletions packages/core/testing/src/test_bed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,12 @@ export class TestBed implements Injector {
}

const ngZone = new NgZone({enableLongStackTrace: true});
const ngZoneInjector =
Injector.create([{provide: NgZone, useValue: ngZone}], this.platform.injector);
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
const ngZoneInjector = Injector.create({
providers: providers,
parent: this.platform.injector,
name: this._moduleFactory.moduleType.name
});
this._moduleRef = this._moduleFactory.create(ngZoneInjector);
// ApplicationInitStatus.runInitializers() is marked @internal to core. So casting to any
// before accessing it.
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/core/di/ts/provider_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export function main() {
name = 'square';
}

const injector = Injector.create([{provide: Square, deps: []}]);
const injector = Injector.create({providers: [{provide: Square, deps: []}]});

const shape: Square = injector.get(Square);
expect(shape.name).toEqual('square');
Expand Down
45 changes: 40 additions & 5 deletions packages/platform-browser/test/browser/bootstrap_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
*/

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 {Console} from '@angular/core/src/console';
import {ComponentRef} from '@angular/core/src/linker/component_factory';
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 {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
Expand Down Expand Up @@ -112,10 +112,11 @@ class DummyConsole implements Console {


class TestModule {}
function bootstrap(cmpType: any, providers: Provider[] = [], platformProviders: StaticProvider[] = [
]): Promise<any> {
function bootstrap(
cmpType: any, providers: Provider[] = [], platformProviders: StaticProvider[] = [],
imports: Type<any>[] = []): Promise<any> {
@NgModule({
imports: [BrowserModule],
imports: [BrowserModule, ...imports],
declarations: [cmpType],
bootstrap: [cmpType],
providers: providers,
Expand Down Expand Up @@ -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()) {
it('should forward the error to promise when bootstrap fails',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
Expand Down
7 changes: 4 additions & 3 deletions packages/upgrade/src/common/downgrade_component_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* 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 {PropertyBinding} from './component_info';
Expand Down Expand Up @@ -54,8 +54,9 @@ export class DowngradeComponentAdapter {
}

createComponent(projectableNodes: Node[][]) {
const childInjector =
Injector.create([{provide: $SCOPE, useValue: this.componentScope}], this.parentInjector);
const providers: StaticProvider[] = [{provide: $SCOPE, useValue: this.componentScope}];
const childInjector = Injector.create(
{providers: providers, parent: this.parentInjector, name: 'DowngradeComponentAdapter'});

this.componentRef =
this.componentFactory.create(childInjector, projectableNodes, this.element[0]);
Expand Down
7 changes: 6 additions & 1 deletion tools/public_api_guard/core/core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,12 @@ export declare abstract class Injector {
/** @deprecated */ abstract get(token: any, notFoundValue?: any): any;
static NULL: Injector;
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 */
Expand Down

0 comments on commit b7738e1

Please sign in to comment.