feat(di): added context to runtime DI errors
This commit is contained in:
parent
8ecb632d70
commit
5a86f85936
|
@ -419,6 +419,9 @@ export class ProtoElementInjector {
|
||||||
getBindingAtIndex(index: number): any { return this.protoInjector.getBindingAtIndex(index); }
|
getBindingAtIndex(index: number): any { return this.protoInjector.getBindingAtIndex(index); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _Context {
|
||||||
|
constructor(public element: any, public componentElement: any, public injector: any) {}
|
||||||
|
}
|
||||||
|
|
||||||
export class ElementInjector extends TreeNode<ElementInjector> implements DependencyProvider {
|
export class ElementInjector extends TreeNode<ElementInjector> implements DependencyProvider {
|
||||||
private _host: ElementInjector;
|
private _host: ElementInjector;
|
||||||
|
@ -438,7 +441,9 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
|
||||||
constructor(public _proto: ProtoElementInjector, parent: ElementInjector) {
|
constructor(public _proto: ProtoElementInjector, parent: ElementInjector) {
|
||||||
super(parent);
|
super(parent);
|
||||||
|
|
||||||
this._injector = new Injector(this._proto.protoInjector, null, this);
|
this._injector =
|
||||||
|
new Injector(this._proto.protoInjector, null, this, () => this._debugContext());
|
||||||
|
|
||||||
// we couple ourselves to the injector strategy to avoid polymoprhic calls
|
// we couple ourselves to the injector strategy to avoid polymoprhic calls
|
||||||
var injectorStrategy = <any>this._injector.internalStrategy;
|
var injectorStrategy = <any>this._injector.internalStrategy;
|
||||||
this._strategy = injectorStrategy instanceof InjectorInlineStrategy ?
|
this._strategy = injectorStrategy instanceof InjectorInlineStrategy ?
|
||||||
|
@ -489,6 +494,12 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
|
||||||
this.hydrated = true;
|
this.hydrated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _debugContext(): any {
|
||||||
|
var p = this._preBuiltObjects;
|
||||||
|
return new _Context(p.elementRef.nativeElement, p.view.getHostElement().nativeElement,
|
||||||
|
this._injector);
|
||||||
|
}
|
||||||
|
|
||||||
private _reattachInjectors(imperativelyCreatedInjector: Injector): void {
|
private _reattachInjectors(imperativelyCreatedInjector: Injector): void {
|
||||||
// Dynamically-loaded component in the template. Not a root ElementInjector.
|
// Dynamically-loaded component in the template. Not a root ElementInjector.
|
||||||
if (isPresent(this._parent)) {
|
if (isPresent(this._parent)) {
|
||||||
|
@ -613,7 +624,7 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NoBindingError(dirDep.key);
|
throw new NoBindingError(null, dirDep.key);
|
||||||
}
|
}
|
||||||
return this._preBuiltObjects.templateRef;
|
return this._preBuiltObjects.templateRef;
|
||||||
}
|
}
|
||||||
|
|
|
@ -200,6 +200,11 @@ export class AppView implements ChangeDispatcher, RenderEventDispatcher {
|
||||||
return isPresent(viewIndex) ? this.views[viewIndex] : null;
|
return isPresent(viewIndex) ? this.views[viewIndex] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getHostElement(): ElementRef {
|
||||||
|
var boundElementIndex = this.mainMergeMapping.hostElementIndicesByViewIndex[this.viewOffset];
|
||||||
|
return this.elementRefs[boundElementIndex];
|
||||||
|
}
|
||||||
|
|
||||||
getDetectorFor(directive: DirectiveIndex): any {
|
getDetectorFor(directive: DirectiveIndex): any {
|
||||||
var childView = this.getNestedView(this.elementOffset + directive.elementIndex);
|
var childView = this.getNestedView(this.elementOffset + directive.elementIndex);
|
||||||
return isPresent(childView) ? childView.changeDetector : null;
|
return isPresent(childView) ? childView.changeDetector : null;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import {ListWrapper, List} from 'angular2/src/facade/collection';
|
import {ListWrapper, List} from 'angular2/src/facade/collection';
|
||||||
import {stringify, BaseException, isBlank} from 'angular2/src/facade/lang';
|
import {stringify, BaseException, isBlank} from 'angular2/src/facade/lang';
|
||||||
|
import {Key} from './key';
|
||||||
|
import {Injector} from './injector';
|
||||||
|
|
||||||
function findFirstClosedCycle(keys: List<any>): List<any> {
|
function findFirstClosedCycle(keys: List<any>): List<any> {
|
||||||
var res = [];
|
var res = [];
|
||||||
|
@ -31,22 +33,27 @@ function constructResolvingPath(keys: List<any>): string {
|
||||||
export class AbstractBindingError extends BaseException {
|
export class AbstractBindingError extends BaseException {
|
||||||
name: string;
|
name: string;
|
||||||
message: string;
|
message: string;
|
||||||
keys: List<any>;
|
keys: List<Key>;
|
||||||
|
injectors: List<Injector>;
|
||||||
constructResolvingMessage: Function;
|
constructResolvingMessage: Function;
|
||||||
// TODO(tbosch): Can't do key:Key as this results in a circular dependency!
|
|
||||||
constructor(key, constructResolvingMessage: Function, originalException?, originalStack?) {
|
constructor(injector: Injector, key: Key, constructResolvingMessage: Function, originalException?,
|
||||||
super(null, originalException, originalStack);
|
originalStack?) {
|
||||||
|
super("DI Exception", originalException, originalStack, null);
|
||||||
this.keys = [key];
|
this.keys = [key];
|
||||||
|
this.injectors = [injector];
|
||||||
this.constructResolvingMessage = constructResolvingMessage;
|
this.constructResolvingMessage = constructResolvingMessage;
|
||||||
this.message = this.constructResolvingMessage(this.keys);
|
this.message = this.constructResolvingMessage(this.keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(tbosch): Can't do key:Key as this results in a circular dependency!
|
addKey(injector: Injector, key: Key): void {
|
||||||
addKey(key: any): void {
|
this.injectors.push(injector);
|
||||||
this.keys.push(key);
|
this.keys.push(key);
|
||||||
this.message = this.constructResolvingMessage(this.keys);
|
this.message = this.constructResolvingMessage(this.keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get context() { return this.injectors[this.injectors.length - 1].debugContext(); }
|
||||||
|
|
||||||
toString(): string { return this.message; }
|
toString(): string { return this.message; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,47 +62,14 @@ export class AbstractBindingError extends BaseException {
|
||||||
* {@link Injector} does not have a {@link Binding} for {@link Key}.
|
* {@link Injector} does not have a {@link Binding} for {@link Key}.
|
||||||
*/
|
*/
|
||||||
export class NoBindingError extends AbstractBindingError {
|
export class NoBindingError extends AbstractBindingError {
|
||||||
// TODO(tbosch): Can't do key:Key as this results in a circular dependency!
|
constructor(injector: Injector, key: Key) {
|
||||||
constructor(key) {
|
super(injector, key, function(keys: List<any>) {
|
||||||
super(key, function(keys: List<any>) {
|
|
||||||
var first = stringify(ListWrapper.first(keys).token);
|
var first = stringify(ListWrapper.first(keys).token);
|
||||||
return `No provider for ${first}!${constructResolvingPath(keys)}`;
|
return `No provider for ${first}!${constructResolvingPath(keys)}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Thrown when trying to retrieve an async {@link Binding} using the sync API.
|
|
||||||
*
|
|
||||||
* ## Example
|
|
||||||
*
|
|
||||||
* ```javascript
|
|
||||||
* var injector = Injector.resolveAndCreate([
|
|
||||||
* bind(Number).toAsyncFactory(() => {
|
|
||||||
* return new Promise((resolve) => resolve(1 + 2));
|
|
||||||
* }),
|
|
||||||
* bind(String).toFactory((v) => { return "Value: " + v; }, [String])
|
|
||||||
* ]);
|
|
||||||
*
|
|
||||||
* injector.asyncGet(String).then((v) => expect(v).toBe('Value: 3'));
|
|
||||||
* expect(() => {
|
|
||||||
* injector.get(String);
|
|
||||||
* }).toThrowError(AsycBindingError);
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The above example throws because `String` depends on `Number` which is async. If any binding in
|
|
||||||
* the dependency graph is async then the graph can only be retrieved using the `asyncGet` API.
|
|
||||||
*/
|
|
||||||
export class AsyncBindingError extends AbstractBindingError {
|
|
||||||
// TODO(tbosch): Can't do key:Key as this results in a circular dependency!
|
|
||||||
constructor(key) {
|
|
||||||
super(key, function(keys: List<any>) {
|
|
||||||
var first = stringify(ListWrapper.first(keys).token);
|
|
||||||
return `Cannot instantiate ${first} synchronously. It is provided as a promise!${constructResolvingPath(keys)}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thrown when dependencies form a cycle.
|
* Thrown when dependencies form a cycle.
|
||||||
*
|
*
|
||||||
|
@ -113,9 +87,8 @@ export class AsyncBindingError extends AbstractBindingError {
|
||||||
* Retrieving `A` or `B` throws a `CyclicDependencyError` as the graph above cannot be constructed.
|
* Retrieving `A` or `B` throws a `CyclicDependencyError` as the graph above cannot be constructed.
|
||||||
*/
|
*/
|
||||||
export class CyclicDependencyError extends AbstractBindingError {
|
export class CyclicDependencyError extends AbstractBindingError {
|
||||||
// TODO(tbosch): Can't do key:Key as this results in a circular dependency!
|
constructor(injector: Injector, key: Key) {
|
||||||
constructor(key) {
|
super(injector, key, function(keys: List<any>) {
|
||||||
super(key, function(keys: List<any>) {
|
|
||||||
return `Cannot instantiate cyclic dependency!${constructResolvingPath(keys)}`;
|
return `Cannot instantiate cyclic dependency!${constructResolvingPath(keys)}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -128,14 +101,13 @@ export class CyclicDependencyError extends AbstractBindingError {
|
||||||
* this object to be instantiated.
|
* this object to be instantiated.
|
||||||
*/
|
*/
|
||||||
export class InstantiationError extends AbstractBindingError {
|
export class InstantiationError extends AbstractBindingError {
|
||||||
causeKey;
|
causeKey: Key;
|
||||||
|
constructor(injector: Injector, originalException, originalStack, key: Key) {
|
||||||
// TODO(tbosch): Can't do key:Key as this results in a circular dependency!
|
super(injector, key, function(keys: List<any>) {
|
||||||
constructor(originalException, originalStack, key) {
|
|
||||||
super(key, function(keys: List<any>) {
|
|
||||||
var first = stringify(ListWrapper.first(keys).token);
|
var first = stringify(ListWrapper.first(keys).token);
|
||||||
return `Error during instantiation of ${first}!${constructResolvingPath(keys)}.` +
|
return `Error during instantiation of ${first}!${constructResolvingPath(keys)}.` +
|
||||||
` ORIGINAL ERROR: ${originalException}` + `\n\n ORIGINAL STACK: ${originalStack}`;
|
`\n\n ORIGINAL ERROR: ${originalException}` +
|
||||||
|
`\n\n ORIGINAL STACK: ${originalStack} \n`;
|
||||||
}, originalException, originalStack);
|
}, originalException, originalStack);
|
||||||
|
|
||||||
this.causeKey = key;
|
this.causeKey = key;
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {ResolvedBinding, Binding, Dependency, BindingBuilder, bind} from './bind
|
||||||
import {
|
import {
|
||||||
AbstractBindingError,
|
AbstractBindingError,
|
||||||
NoBindingError,
|
NoBindingError,
|
||||||
AsyncBindingError,
|
|
||||||
CyclicDependencyError,
|
CyclicDependencyError,
|
||||||
InstantiationError,
|
InstantiationError,
|
||||||
InvalidBindingError,
|
InvalidBindingError,
|
||||||
|
@ -174,8 +173,10 @@ export class ProtoInjectorDynamicStrategy implements ProtoInjectorStrategy {
|
||||||
|
|
||||||
export class ProtoInjector {
|
export class ProtoInjector {
|
||||||
_strategy: ProtoInjectorStrategy;
|
_strategy: ProtoInjectorStrategy;
|
||||||
|
numberOfBindings: number;
|
||||||
|
|
||||||
constructor(bwv: BindingWithVisibility[]) {
|
constructor(bwv: BindingWithVisibility[]) {
|
||||||
|
this.numberOfBindings = bwv.length;
|
||||||
this._strategy = bwv.length > _MAX_CONSTRUCTION_COUNTER ?
|
this._strategy = bwv.length > _MAX_CONSTRUCTION_COUNTER ?
|
||||||
new ProtoInjectorDynamicStrategy(this, bwv) :
|
new ProtoInjectorDynamicStrategy(this, bwv) :
|
||||||
new ProtoInjectorInlineStrategy(this, bwv);
|
new ProtoInjectorInlineStrategy(this, bwv);
|
||||||
|
@ -469,10 +470,18 @@ export class Injector {
|
||||||
_constructionCounter: number = 0;
|
_constructionCounter: number = 0;
|
||||||
|
|
||||||
constructor(public _proto: ProtoInjector, public _parent: Injector = null,
|
constructor(public _proto: ProtoInjector, public _parent: Injector = null,
|
||||||
private _depProvider: DependencyProvider = null) {
|
private _depProvider: DependencyProvider = null,
|
||||||
|
private _debugContext: Function = null) {
|
||||||
this._strategy = _proto._strategy.createInjectorStrategy(this);
|
this._strategy = _proto._strategy.createInjectorStrategy(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns debug information about the injector.
|
||||||
|
*
|
||||||
|
* This information is included into exceptions thrown by the injector.
|
||||||
|
*/
|
||||||
|
debugContext(): any { return this._debugContext(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves an instance from the injector.
|
* Retrieves an instance from the injector.
|
||||||
*
|
*
|
||||||
|
@ -550,7 +559,7 @@ export class Injector {
|
||||||
|
|
||||||
_new(binding: ResolvedBinding, visibility: number): any {
|
_new(binding: ResolvedBinding, visibility: number): any {
|
||||||
if (this._constructionCounter++ > this._strategy.getMaxNumberOfObjects()) {
|
if (this._constructionCounter++ > this._strategy.getMaxNumberOfObjects()) {
|
||||||
throw new CyclicDependencyError(binding.key);
|
throw new CyclicDependencyError(this, binding.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
var factory = binding.factory;
|
var factory = binding.factory;
|
||||||
|
@ -580,7 +589,9 @@ export class Injector {
|
||||||
d18 = length > 18 ? this._getByDependency(binding, deps[18], visibility) : null;
|
d18 = length > 18 ? this._getByDependency(binding, deps[18], visibility) : null;
|
||||||
d19 = length > 19 ? this._getByDependency(binding, deps[19], visibility) : null;
|
d19 = length > 19 ? this._getByDependency(binding, deps[19], visibility) : null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof AbstractBindingError) e.addKey(binding.key);
|
if (e instanceof AbstractBindingError) {
|
||||||
|
e.addKey(this, binding.key);
|
||||||
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -655,7 +666,7 @@ export class Injector {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new InstantiationError(e, e.stack, binding.key);
|
throw new InstantiationError(this, e, e.stack, binding.key);
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
@ -693,7 +704,7 @@ export class Injector {
|
||||||
if (optional) {
|
if (optional) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
throw new NoBindingError(key);
|
throw new NoBindingError(this, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -751,6 +762,12 @@ export class Injector {
|
||||||
|
|
||||||
return this._throwOrNull(key, optional);
|
return this._throwOrNull(key, optional);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get displayName(): string {
|
||||||
|
return `Injector(bindings: [${_mapBindings(this, b => ` "${b.key.displayName}" `).join(", ")}])`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string { return this.displayName; }
|
||||||
}
|
}
|
||||||
|
|
||||||
var INJECTOR_KEY = Key.get(Injector);
|
var INJECTOR_KEY = Key.get(Injector);
|
||||||
|
@ -795,3 +812,11 @@ function _flattenBindings(bindings: List<ResolvedBinding | List<any>>,
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _mapBindings(injector: Injector, fn: Function): any[] {
|
||||||
|
var res = [];
|
||||||
|
for (var i = 0; i < injector._proto.numberOfBindings; ++i) {
|
||||||
|
res.push(fn(injector._proto.getBindingAtIndex(i)));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
|
@ -1154,6 +1154,22 @@ export function main() {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should provide an error context when an error happens in the DI',
|
||||||
|
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||||
|
|
||||||
|
tcb = tcb.overrideView(MyComp, new viewAnn.View({
|
||||||
|
directives: [DirectiveThrowingAnError],
|
||||||
|
template: `<directive-throwing-error></<directive-throwing-error>`
|
||||||
|
}));
|
||||||
|
|
||||||
|
PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => {
|
||||||
|
expect(DOM.nodeName(e.context.element).toUpperCase())
|
||||||
|
.toEqual("DIRECTIVE-THROWING-ERROR");
|
||||||
|
async.done();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
if (!IS_DARTIUM) {
|
if (!IS_DARTIUM) {
|
||||||
it('should report a meaningful error when a directive is undefined',
|
it('should report a meaningful error when a directive is undefined',
|
||||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder,
|
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder,
|
||||||
|
@ -1870,3 +1886,11 @@ class OtherDuplicateDir {
|
||||||
DOM.setText(elRef.nativeElement, DOM.getText(elRef.nativeElement) + 'othernoduplicate');
|
DOM.setText(elRef.nativeElement, DOM.getText(elRef.nativeElement) + 'othernoduplicate');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Directive({selector: 'directive-throwing-error'})
|
||||||
|
class DirectiveThrowingAnError {
|
||||||
|
constructor() {
|
||||||
|
throw new BaseException("BOOM");
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -292,7 +292,12 @@ export function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show the full path when error happens in a constructor', () => {
|
it('should show the full path when error happens in a constructor', () => {
|
||||||
var injector = createInjector([Car, bind(Engine).toClass(BrokenEngine)]);
|
var bindings = Injector.resolve([Car, bind(Engine).toClass(BrokenEngine)]);
|
||||||
|
var proto = new ProtoInjector([
|
||||||
|
new BindingWithVisibility(bindings[0], PUBLIC),
|
||||||
|
new BindingWithVisibility(bindings[1], PUBLIC)
|
||||||
|
]);
|
||||||
|
var injector = new Injector(proto, null, null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
injector.get(Car);
|
injector.get(Car);
|
||||||
|
@ -305,6 +310,24 @@ export function main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should provide context when throwing an exception ', () => {
|
||||||
|
var engineBinding = Injector.resolve([bind(Engine).toClass(BrokenEngine)])[0];
|
||||||
|
var protoParent = new ProtoInjector([new BindingWithVisibility(engineBinding, PUBLIC)]);
|
||||||
|
|
||||||
|
var carBinding = Injector.resolve([Car])[0];
|
||||||
|
var protoChild = new ProtoInjector([new BindingWithVisibility(carBinding, PUBLIC)]);
|
||||||
|
|
||||||
|
var parent = new Injector(protoParent, null, null, () => "parentContext");
|
||||||
|
var child = new Injector(protoChild, parent, null, () => "childContext");
|
||||||
|
|
||||||
|
try {
|
||||||
|
child.get(Car);
|
||||||
|
throw "Must throw";
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.context).toEqual("childContext");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should instantiate an object after a failed attempt', () => {
|
it('should instantiate an object after a failed attempt', () => {
|
||||||
var isBroken = true;
|
var isBroken = true;
|
||||||
|
|
||||||
|
@ -545,5 +568,12 @@ export function main() {
|
||||||
expect(binding.dependencies[0].properties).toEqual([new CustomDependencyMetadata()]);
|
expect(binding.dependencies[0].properties).toEqual([new CustomDependencyMetadata()]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("displayName", () => {
|
||||||
|
it("should work", () => {
|
||||||
|
expect(Injector.resolveAndCreate([Engine, BrokenEngine]).displayName)
|
||||||
|
.toEqual('Injector(bindings: [ "Engine" , "BrokenEngine" ])');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue