X Tutup
Skip to content

Commit 5a86f85

Browse files
committed
feat(di): added context to runtime DI errors
1 parent 8ecb632 commit 5a86f85

File tree

6 files changed

+126
-59
lines changed

6 files changed

+126
-59
lines changed

modules/angular2/src/core/compiler/element_injector.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,9 @@ export class ProtoElementInjector {
419419
getBindingAtIndex(index: number): any { return this.protoInjector.getBindingAtIndex(index); }
420420
}
421421

422+
class _Context {
423+
constructor(public element: any, public componentElement: any, public injector: any) {}
424+
}
422425

423426
export class ElementInjector extends TreeNode<ElementInjector> implements DependencyProvider {
424427
private _host: ElementInjector;
@@ -438,7 +441,9 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
438441
constructor(public _proto: ProtoElementInjector, parent: ElementInjector) {
439442
super(parent);
440443

441-
this._injector = new Injector(this._proto.protoInjector, null, this);
444+
this._injector =
445+
new Injector(this._proto.protoInjector, null, this, () => this._debugContext());
446+
442447
// we couple ourselves to the injector strategy to avoid polymoprhic calls
443448
var injectorStrategy = <any>this._injector.internalStrategy;
444449
this._strategy = injectorStrategy instanceof InjectorInlineStrategy ?
@@ -489,6 +494,12 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
489494
this.hydrated = true;
490495
}
491496

497+
private _debugContext(): any {
498+
var p = this._preBuiltObjects;
499+
return new _Context(p.elementRef.nativeElement, p.view.getHostElement().nativeElement,
500+
this._injector);
501+
}
502+
492503
private _reattachInjectors(imperativelyCreatedInjector: Injector): void {
493504
// Dynamically-loaded component in the template. Not a root ElementInjector.
494505
if (isPresent(this._parent)) {
@@ -613,7 +624,7 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
613624
return null;
614625
}
615626

616-
throw new NoBindingError(dirDep.key);
627+
throw new NoBindingError(null, dirDep.key);
617628
}
618629
return this._preBuiltObjects.templateRef;
619630
}

modules/angular2/src/core/compiler/view.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@ export class AppView implements ChangeDispatcher, RenderEventDispatcher {
200200
return isPresent(viewIndex) ? this.views[viewIndex] : null;
201201
}
202202

203+
getHostElement(): ElementRef {
204+
var boundElementIndex = this.mainMergeMapping.hostElementIndicesByViewIndex[this.viewOffset];
205+
return this.elementRefs[boundElementIndex];
206+
}
207+
203208
getDetectorFor(directive: DirectiveIndex): any {
204209
var childView = this.getNestedView(this.elementOffset + directive.elementIndex);
205210
return isPresent(childView) ? childView.changeDetector : null;

modules/angular2/src/di/exceptions.ts

Lines changed: 22 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {ListWrapper, List} from 'angular2/src/facade/collection';
22
import {stringify, BaseException, isBlank} from 'angular2/src/facade/lang';
3+
import {Key} from './key';
4+
import {Injector} from './injector';
35

46
function findFirstClosedCycle(keys: List<any>): List<any> {
57
var res = [];
@@ -31,22 +33,27 @@ function constructResolvingPath(keys: List<any>): string {
3133
export class AbstractBindingError extends BaseException {
3234
name: string;
3335
message: string;
34-
keys: List<any>;
36+
keys: List<Key>;
37+
injectors: List<Injector>;
3538
constructResolvingMessage: Function;
36-
// TODO(tbosch): Can't do key:Key as this results in a circular dependency!
37-
constructor(key, constructResolvingMessage: Function, originalException?, originalStack?) {
38-
super(null, originalException, originalStack);
39+
40+
constructor(injector: Injector, key: Key, constructResolvingMessage: Function, originalException?,
41+
originalStack?) {
42+
super("DI Exception", originalException, originalStack, null);
3943
this.keys = [key];
44+
this.injectors = [injector];
4045
this.constructResolvingMessage = constructResolvingMessage;
4146
this.message = this.constructResolvingMessage(this.keys);
4247
}
4348

44-
// TODO(tbosch): Can't do key:Key as this results in a circular dependency!
45-
addKey(key: any): void {
49+
addKey(injector: Injector, key: Key): void {
50+
this.injectors.push(injector);
4651
this.keys.push(key);
4752
this.message = this.constructResolvingMessage(this.keys);
4853
}
4954

55+
get context() { return this.injectors[this.injectors.length - 1].debugContext(); }
56+
5057
toString(): string { return this.message; }
5158
}
5259

@@ -55,47 +62,14 @@ export class AbstractBindingError extends BaseException {
5562
* {@link Injector} does not have a {@link Binding} for {@link Key}.
5663
*/
5764
export class NoBindingError extends AbstractBindingError {
58-
// TODO(tbosch): Can't do key:Key as this results in a circular dependency!
59-
constructor(key) {
60-
super(key, function(keys: List<any>) {
65+
constructor(injector: Injector, key: Key) {
66+
super(injector, key, function(keys: List<any>) {
6167
var first = stringify(ListWrapper.first(keys).token);
6268
return `No provider for ${first}!${constructResolvingPath(keys)}`;
6369
});
6470
}
6571
}
6672

67-
/**
68-
* Thrown when trying to retrieve an async {@link Binding} using the sync API.
69-
*
70-
* ## Example
71-
*
72-
* ```javascript
73-
* var injector = Injector.resolveAndCreate([
74-
* bind(Number).toAsyncFactory(() => {
75-
* return new Promise((resolve) => resolve(1 + 2));
76-
* }),
77-
* bind(String).toFactory((v) => { return "Value: " + v; }, [String])
78-
* ]);
79-
*
80-
* injector.asyncGet(String).then((v) => expect(v).toBe('Value: 3'));
81-
* expect(() => {
82-
* injector.get(String);
83-
* }).toThrowError(AsycBindingError);
84-
* ```
85-
*
86-
* The above example throws because `String` depends on `Number` which is async. If any binding in
87-
* the dependency graph is async then the graph can only be retrieved using the `asyncGet` API.
88-
*/
89-
export class AsyncBindingError extends AbstractBindingError {
90-
// TODO(tbosch): Can't do key:Key as this results in a circular dependency!
91-
constructor(key) {
92-
super(key, function(keys: List<any>) {
93-
var first = stringify(ListWrapper.first(keys).token);
94-
return `Cannot instantiate ${first} synchronously. It is provided as a promise!${constructResolvingPath(keys)}`;
95-
});
96-
}
97-
}
98-
9973
/**
10074
* Thrown when dependencies form a cycle.
10175
*
@@ -113,9 +87,8 @@ export class AsyncBindingError extends AbstractBindingError {
11387
* Retrieving `A` or `B` throws a `CyclicDependencyError` as the graph above cannot be constructed.
11488
*/
11589
export class CyclicDependencyError extends AbstractBindingError {
116-
// TODO(tbosch): Can't do key:Key as this results in a circular dependency!
117-
constructor(key) {
118-
super(key, function(keys: List<any>) {
90+
constructor(injector: Injector, key: Key) {
91+
super(injector, key, function(keys: List<any>) {
11992
return `Cannot instantiate cyclic dependency!${constructResolvingPath(keys)}`;
12093
});
12194
}
@@ -128,14 +101,13 @@ export class CyclicDependencyError extends AbstractBindingError {
128101
* this object to be instantiated.
129102
*/
130103
export class InstantiationError extends AbstractBindingError {
131-
causeKey;
132-
133-
// TODO(tbosch): Can't do key:Key as this results in a circular dependency!
134-
constructor(originalException, originalStack, key) {
135-
super(key, function(keys: List<any>) {
104+
causeKey: Key;
105+
constructor(injector: Injector, originalException, originalStack, key: Key) {
106+
super(injector, key, function(keys: List<any>) {
136107
var first = stringify(ListWrapper.first(keys).token);
137108
return `Error during instantiation of ${first}!${constructResolvingPath(keys)}.` +
138-
` ORIGINAL ERROR: ${originalException}` + `\n\n ORIGINAL STACK: ${originalStack}`;
109+
`\n\n ORIGINAL ERROR: ${originalException}` +
110+
`\n\n ORIGINAL STACK: ${originalStack} \n`;
139111
}, originalException, originalStack);
140112

141113
this.causeKey = key;

modules/angular2/src/di/injector.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {ResolvedBinding, Binding, Dependency, BindingBuilder, bind} from './bind
55
import {
66
AbstractBindingError,
77
NoBindingError,
8-
AsyncBindingError,
98
CyclicDependencyError,
109
InstantiationError,
1110
InvalidBindingError,
@@ -174,8 +173,10 @@ export class ProtoInjectorDynamicStrategy implements ProtoInjectorStrategy {
174173

175174
export class ProtoInjector {
176175
_strategy: ProtoInjectorStrategy;
176+
numberOfBindings: number;
177177

178178
constructor(bwv: BindingWithVisibility[]) {
179+
this.numberOfBindings = bwv.length;
179180
this._strategy = bwv.length > _MAX_CONSTRUCTION_COUNTER ?
180181
new ProtoInjectorDynamicStrategy(this, bwv) :
181182
new ProtoInjectorInlineStrategy(this, bwv);
@@ -469,10 +470,18 @@ export class Injector {
469470
_constructionCounter: number = 0;
470471

471472
constructor(public _proto: ProtoInjector, public _parent: Injector = null,
472-
private _depProvider: DependencyProvider = null) {
473+
private _depProvider: DependencyProvider = null,
474+
private _debugContext: Function = null) {
473475
this._strategy = _proto._strategy.createInjectorStrategy(this);
474476
}
475477

478+
/**
479+
* Returns debug information about the injector.
480+
*
481+
* This information is included into exceptions thrown by the injector.
482+
*/
483+
debugContext(): any { return this._debugContext(); }
484+
476485
/**
477486
* Retrieves an instance from the injector.
478487
*
@@ -550,7 +559,7 @@ export class Injector {
550559

551560
_new(binding: ResolvedBinding, visibility: number): any {
552561
if (this._constructionCounter++ > this._strategy.getMaxNumberOfObjects()) {
553-
throw new CyclicDependencyError(binding.key);
562+
throw new CyclicDependencyError(this, binding.key);
554563
}
555564

556565
var factory = binding.factory;
@@ -580,7 +589,9 @@ export class Injector {
580589
d18 = length > 18 ? this._getByDependency(binding, deps[18], visibility) : null;
581590
d19 = length > 19 ? this._getByDependency(binding, deps[19], visibility) : null;
582591
} catch (e) {
583-
if (e instanceof AbstractBindingError) e.addKey(binding.key);
592+
if (e instanceof AbstractBindingError) {
593+
e.addKey(this, binding.key);
594+
}
584595
throw e;
585596
}
586597

@@ -655,7 +666,7 @@ export class Injector {
655666
break;
656667
}
657668
} catch (e) {
658-
throw new InstantiationError(e, e.stack, binding.key);
669+
throw new InstantiationError(this, e, e.stack, binding.key);
659670
}
660671
return obj;
661672
}
@@ -693,7 +704,7 @@ export class Injector {
693704
if (optional) {
694705
return null;
695706
} else {
696-
throw new NoBindingError(key);
707+
throw new NoBindingError(this, key);
697708
}
698709
}
699710

@@ -751,6 +762,12 @@ export class Injector {
751762

752763
return this._throwOrNull(key, optional);
753764
}
765+
766+
get displayName(): string {
767+
return `Injector(bindings: [${_mapBindings(this, b => ` "${b.key.displayName}" `).join(", ")}])`;
768+
}
769+
770+
toString(): string { return this.displayName; }
754771
}
755772

756773
var INJECTOR_KEY = Key.get(Injector);
@@ -795,3 +812,11 @@ function _flattenBindings(bindings: List<ResolvedBinding | List<any>>,
795812
});
796813
return res;
797814
}
815+
816+
function _mapBindings(injector: Injector, fn: Function): any[] {
817+
var res = [];
818+
for (var i = 0; i < injector._proto.numberOfBindings; ++i) {
819+
res.push(fn(injector._proto.getBindingAtIndex(i)));
820+
}
821+
return res;
822+
}

modules/angular2/test/core/compiler/integration_spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,22 @@ export function main() {
11541154
});
11551155
}));
11561156

1157+
it('should provide an error context when an error happens in the DI',
1158+
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
1159+
1160+
tcb = tcb.overrideView(MyComp, new viewAnn.View({
1161+
directives: [DirectiveThrowingAnError],
1162+
template: `<directive-throwing-error></<directive-throwing-error>`
1163+
}));
1164+
1165+
PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => {
1166+
expect(DOM.nodeName(e.context.element).toUpperCase())
1167+
.toEqual("DIRECTIVE-THROWING-ERROR");
1168+
async.done();
1169+
return null;
1170+
});
1171+
}));
1172+
11571173
if (!IS_DARTIUM) {
11581174
it('should report a meaningful error when a directive is undefined',
11591175
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder,
@@ -1870,3 +1886,11 @@ class OtherDuplicateDir {
18701886
DOM.setText(elRef.nativeElement, DOM.getText(elRef.nativeElement) + 'othernoduplicate');
18711887
}
18721888
}
1889+
1890+
@Directive({selector: 'directive-throwing-error'})
1891+
class DirectiveThrowingAnError {
1892+
constructor() {
1893+
throw new BaseException("BOOM");
1894+
;
1895+
}
1896+
}

modules/angular2/test/di/injector_spec.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,12 @@ export function main() {
292292
});
293293

294294
it('should show the full path when error happens in a constructor', () => {
295-
var injector = createInjector([Car, bind(Engine).toClass(BrokenEngine)]);
295+
var bindings = Injector.resolve([Car, bind(Engine).toClass(BrokenEngine)]);
296+
var proto = new ProtoInjector([
297+
new BindingWithVisibility(bindings[0], PUBLIC),
298+
new BindingWithVisibility(bindings[1], PUBLIC)
299+
]);
300+
var injector = new Injector(proto, null, null);
296301

297302
try {
298303
injector.get(Car);
@@ -305,6 +310,24 @@ export function main() {
305310
}
306311
});
307312

313+
it('should provide context when throwing an exception ', () => {
314+
var engineBinding = Injector.resolve([bind(Engine).toClass(BrokenEngine)])[0];
315+
var protoParent = new ProtoInjector([new BindingWithVisibility(engineBinding, PUBLIC)]);
316+
317+
var carBinding = Injector.resolve([Car])[0];
318+
var protoChild = new ProtoInjector([new BindingWithVisibility(carBinding, PUBLIC)]);
319+
320+
var parent = new Injector(protoParent, null, null, () => "parentContext");
321+
var child = new Injector(protoChild, parent, null, () => "childContext");
322+
323+
try {
324+
child.get(Car);
325+
throw "Must throw";
326+
} catch (e) {
327+
expect(e.context).toEqual("childContext");
328+
}
329+
});
330+
308331
it('should instantiate an object after a failed attempt', () => {
309332
var isBroken = true;
310333

@@ -545,5 +568,12 @@ export function main() {
545568
expect(binding.dependencies[0].properties).toEqual([new CustomDependencyMetadata()]);
546569
});
547570
});
571+
572+
describe("displayName", () => {
573+
it("should work", () => {
574+
expect(Injector.resolveAndCreate([Engine, BrokenEngine]).displayName)
575+
.toEqual('Injector(bindings: [ "Engine" , "BrokenEngine" ])');
576+
});
577+
});
548578
});
549579
}

0 commit comments

Comments
 (0)
X Tutup