diff --git a/packages/core/src/render3/assert.ts b/packages/core/src/render3/assert.ts index a48c924e19..bda945c9d5 100644 --- a/packages/core/src/render3/assert.ts +++ b/packages/core/src/render3/assert.ts @@ -6,14 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {assertDefined, assertEqual, throwError} from '../util/assert'; - +import {assertDefined, assertEqual, assertIndexInRange, assertNumber, throwError} from '../util/assert'; import {getComponentDef, getNgModuleDef} from './definition'; import {LContainer} from './interfaces/container'; import {DirectiveDef} from './interfaces/definition'; +import { PARENT_INJECTOR } from './interfaces/injector'; import {TNode} from './interfaces/node'; import {isLContainer, isLView} from './interfaces/type_checks'; -import {LView, TVIEW, TView} from './interfaces/view'; +import {HEADER_OFFSET, LView, TVIEW, TView} from './interfaces/view'; // [Assert functions do not constraint type when they are guarded by a truthy // expression.](https://github.com/microsoft/TypeScript/issues/37295) @@ -96,3 +96,55 @@ export function assertDirectiveDef(obj: any): asserts obj is DirectiveDef `Expected a DirectiveDef/ComponentDef and this object does not seem to have the expected shape.`); } } + +export function assertIndexInDeclRange(lView: LView, index: number) { + const tView = lView[1]; + assertBetween(HEADER_OFFSET, tView.bindingStartIndex, index); +} + +export function assertIndexInVarsRange(lView: LView, index: number) { + const tView = lView[1]; + assertBetween( + tView.bindingStartIndex, (tView as any as {i18nStartIndex: number}).i18nStartIndex, index); +} + +export function assertIndexInI18nRange(lView: LView, index: number) { + const tView = lView[1]; + assertBetween( + (tView as any as {i18nStartIndex: number}).i18nStartIndex, tView.expandoStartIndex, index); +} + +export function assertIndexInExpandoRange(lView: LView, index: number) { + const tView = lView[1]; + assertBetween(tView.expandoStartIndex, lView.length, index); +} + +export function assertBetween(lower: number, upper: number, index: number) { + if (!(lower <= index && index < upper)) { + throwError(`Index out of range (expecting ${lower} <= ${index} < ${upper})`); + } +} + + +/** + * This is a basic sanity check that the `injectorIndex` seems to point to what looks like a + * NodeInjector data structure. + * + * @param lView `LView` which should be checked. + * @param injectorIndex index into the `LView` where the `NodeInjector` is expected. + */ +export function assertNodeInjector(lView: LView, injectorIndex: number) { + assertIndexInExpandoRange(lView, injectorIndex); + assertIndexInExpandoRange(lView, injectorIndex + PARENT_INJECTOR); + assertNumber(lView[injectorIndex + 0], 'injectorIndex should point to a bloom filter'); + assertNumber(lView[injectorIndex + 1], 'injectorIndex should point to a bloom filter'); + assertNumber(lView[injectorIndex + 2], 'injectorIndex should point to a bloom filter'); + assertNumber(lView[injectorIndex + 3], 'injectorIndex should point to a bloom filter'); + assertNumber(lView[injectorIndex + 4], 'injectorIndex should point to a bloom filter'); + assertNumber(lView[injectorIndex + 5], 'injectorIndex should point to a bloom filter'); + assertNumber(lView[injectorIndex + 6], 'injectorIndex should point to a bloom filter'); + assertNumber(lView[injectorIndex + 7], 'injectorIndex should point to a bloom filter'); + assertNumber( + lView[injectorIndex + 8 /*PARENT_INJECTOR*/], + 'injectorIndex should point to parent injector'); +} \ No newline at end of file diff --git a/packages/core/src/render3/features/providers_feature.ts b/packages/core/src/render3/features/providers_feature.ts index 4554ab8d80..3c75066753 100644 --- a/packages/core/src/render3/features/providers_feature.ts +++ b/packages/core/src/render3/features/providers_feature.ts @@ -32,7 +32,7 @@ import {DirectiveDef} from '../interfaces/definition'; * ɵɵtextInterpolate(ctx.greeter.greet()); * } * }, - * features: [ProvidersFeature([GreeterDE])] + * features: [ɵɵProvidersFeature([GreeterDE])] * }); * } * ``` diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts index f0ea6e86b4..5521e3a370 100644 --- a/packages/core/src/render3/instructions/lview_debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -6,21 +6,25 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injector, SchemaMetadata} from '../../core'; +import {Injector, SchemaMetadata, Type} from '../../core'; import {Sanitizer} from '../../sanitization/sanitizer'; import {KeyValueArray} from '../../util/array_utils'; import {assertDefined} from '../../util/assert'; import {createNamedArrayType} from '../../util/named_array_type'; import {initNgDevMode} from '../../util/ng_dev_mode'; +import {assertNodeInjector} from '../assert'; +import {getInjectorIndex} from '../di'; import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS, NATIVE} from '../interfaces/container'; -import {ComponentTemplate, DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition'; +import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition'; +import {NO_PARENT_INJECTOR, PARENT_INJECTOR, TNODE} from '../interfaces/injector'; import {AttributeMarker, PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TNodeTypeAsString, TViewNode} from '../interfaces/node'; import {SelectorFlags} from '../interfaces/projection'; import {LQueries, TQueries} from '../interfaces/query'; import {RComment, RElement, Renderer3, RendererFactory3, RNode} from '../interfaces/renderer'; import {getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate, TStylingKey, TStylingRange} from '../interfaces/styling'; -import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DebugNode, DECLARATION_VIEW, DestroyHookData, ExpandoInstructions, FLAGS, HEADER_OFFSET, HookData, HOST, INJECTOR, LContainerDebug as ILContainerDebug, LView, LViewDebug as ILViewDebug, LViewDebugRange, LViewDebugRangeContent, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TView as ITView, TVIEW, TView, TViewType} from '../interfaces/view'; +import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DebugNode, DECLARATION_VIEW, DestroyHookData, ExpandoInstructions, FLAGS, HEADER_OFFSET, HookData, HOST, INJECTOR, LContainerDebug as ILContainerDebug, LView, LViewDebug as ILViewDebug, LViewDebugRange, LViewDebugRangeContent, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TView as ITView, TVIEW, TView, TViewType, TViewTypeAsString} from '../interfaces/view'; import {attachDebugObject} from '../util/debug_utils'; +import {getParentInjectorIndex, getParentInjectorView} from '../util/injector_utils'; import {unwrapRNode} from '../util/view_utils'; const NG_DEV_MODE = ((typeof ngDevMode === 'undefined' || !!ngDevMode) && initNgDevMode()); @@ -152,6 +156,14 @@ export const TViewConstructor = class TView implements ITView { processTNodeChildren(this.firstChild, buf); return buf.join(''); } + + get type_(): string { + return TViewTypeAsString[this.type] || `TViewType.?${this.type}?`; + } + + get i18nStartIndex(): number { + return HEADER_OFFSET + this._decls + this._vars; + } }; class TNode implements ITNode { @@ -189,23 +201,39 @@ class TNode implements ITNode { public styleBindings: TStylingRange, // ) {} - get type_(): string { - switch (this.type) { - case TNodeType.Container: - return 'TNodeType.Container'; - case TNodeType.Element: - return 'TNodeType.Element'; - case TNodeType.ElementContainer: - return 'TNodeType.ElementContainer'; - case TNodeType.IcuContainer: - return 'TNodeType.IcuContainer'; - case TNodeType.Projection: - return 'TNodeType.Projection'; - case TNodeType.View: - return 'TNodeType.View'; - default: - return 'TNodeType.???'; + /** + * Return a human debug version of the set of `NodeInjector`s which will be consulted when + * resolving tokens from this `TNode`. + * + * When debugging applications, it is often difficult to determine which `NodeInjector`s will be + * consulted. This method shows a list of `DebugNode`s representing the `TNode`s which will be + * consulted in order when resolving a token starting at this `TNode`. + * + * The original data is stored in `LView` and `TView` with a lot of offset indexes, and so it is + * difficult to reason about. + * + * @param lView The `LView` instance for this `TNode`. + */ + debugNodeInjectorPath(lView: LView): DebugNode[] { + const path: DebugNode[] = []; + let injectorIndex = getInjectorIndex(this, lView); + ngDevMode && assertNodeInjector(lView, injectorIndex); + while (injectorIndex !== -1) { + const tNode = lView[TVIEW].data[injectorIndex + TNODE] as TNode; + path.push(buildDebugNode(tNode, lView)); + const parentLocation = lView[injectorIndex + PARENT_INJECTOR]; + if (parentLocation === NO_PARENT_INJECTOR) { + injectorIndex = -1; + } else { + injectorIndex = getParentInjectorIndex(parentLocation); + lView = getParentInjectorView(parentLocation, lView); + } } + return path; + } + + get type_(): string { + return TNodeTypeAsString[this.type] || `TNodeType.?${this.type}?`; } get flags_(): string { @@ -246,6 +274,14 @@ class TNode implements ITNode { get classBindings_(): DebugStyleBindings { return toDebugStyleBinding(this, true); } + + get providerIndexStart_(): number { + return this.providerIndexes & TNodeProviderIndexes.ProvidersStartIndexMask; + } + get providerIndexEnd_(): number { + return this.providerIndexStart_ + + (this.providerIndexes >>> TNodeProviderIndexes.CptViewProvidersCountShift); + } } export const TNodeDebug = TNode; export type TNodeDebug = TNode; @@ -462,21 +498,21 @@ export class LViewDebug implements ILViewDebug { } get decls(): LViewDebugRange { - const tView = this.tView as any as {_decls: number, _vars: number}; - const start = HEADER_OFFSET; - return toLViewRange(this.tView, this._raw_lView, start, start + tView._decls); + return toLViewRange(this.tView, this._raw_lView, HEADER_OFFSET, this.tView.bindingStartIndex); } get vars(): LViewDebugRange { - const tView = this.tView as any as {_decls: number, _vars: number}; - const start = HEADER_OFFSET + tView._decls; - return toLViewRange(this.tView, this._raw_lView, start, start + tView._vars); + const tView = this.tView; + return toLViewRange( + tView, this._raw_lView, tView.bindingStartIndex, + (tView as any as {i18nStartIndex: number}).i18nStartIndex); } get i18n(): LViewDebugRange { - const tView = this.tView as any as {_decls: number, _vars: number}; - const start = HEADER_OFFSET + tView._decls + tView._vars; - return toLViewRange(this.tView, this._raw_lView, start, this.tView.expandoStartIndex); + const tView = this.tView; + return toLViewRange( + tView, this._raw_lView, (tView as any as {i18nStartIndex: number}).i18nStartIndex, + tView.expandoStartIndex); } get expando(): LViewDebugRange { @@ -518,7 +554,7 @@ export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[] { const debugNodes: DebugNode[] = []; let tNodeCursor: ITNode|null = tNode; while (tNodeCursor) { - debugNodes.push(buildDebugNode(tNodeCursor, lView, tNodeCursor.index)); + debugNodes.push(buildDebugNode(tNodeCursor, lView)); tNodeCursor = tNodeCursor.next; } return debugNodes; @@ -527,17 +563,75 @@ export function toDebugNodes(tNode: ITNode|null, lView: LView): DebugNode[] { } } -export function buildDebugNode(tNode: ITNode, lView: LView, nodeIndex: number): DebugNode { - const rawValue = lView[nodeIndex]; +export function buildDebugNode(tNode: ITNode, lView: LView): DebugNode { + const rawValue = lView[tNode.index]; const native = unwrapRNode(rawValue); + const factories: Type[] = []; + const instances: any[] = []; + const tView = lView[TVIEW]; + for (let i = tNode.directiveStart; i < tNode.directiveEnd; i++) { + const def = tView.data[i] as DirectiveDef; + factories.push(def.type); + instances.push(lView[i]); + } return { html: toHtml(native), type: TNodeTypeAsString[tNode.type], native: native as any, children: toDebugNodes(tNode.child, lView), + factories, + instances, + injector: buildNodeInjectorDebug(tNode, tView, lView) }; } +function buildNodeInjectorDebug(tNode: ITNode, tView: ITView, lView: LView) { + const viewProviders: Type[] = []; + for (let i = (tNode as TNode).providerIndexStart_; i < (tNode as TNode).providerIndexEnd_; i++) { + viewProviders.push(tView.data[i] as Type); + } + const providers: Type[] = []; + for (let i = (tNode as TNode).providerIndexEnd_; i < (tNode as TNode).directiveEnd; i++) { + providers.push(tView.data[i] as Type); + } + const nodeInjectorDebug = { + bloom: toBloom(lView, tNode.injectorIndex), + cumulativeBloom: toBloom(tView.data, tNode.injectorIndex), + providers, + viewProviders, + parentInjectorIndex: lView[(tNode as TNode).providerIndexStart_ - 1], + }; + return nodeInjectorDebug; +} + +/** + * Convert a number at `idx` location in `array` into binary representation. + * + * @param array + * @param idx + */ +function binary(array: any[], idx: number): string { + const value = array[idx]; + // If not a number we print 8 `?` to retain alignment but let user know that it was called on + // wrong type. + if (typeof value !== 'number') return '????????'; + // We prefix 0s so that we have constant length number + const text = '00000000' + value.toString(2); + return text.substring(text.length - 8); +} + +/** + * Convert a bloom filter at location `idx` in `array` into binary representation. + * + * @param array + * @param idx + */ +function toBloom(array: any[], idx: number): string { + return `${binary(array, idx + 7)}_${binary(array, idx + 6)}_${binary(array, idx + 5)}_${ + binary(array, idx + 4)}_${binary(array, idx + 3)}_${binary(array, idx + 2)}_${ + binary(array, idx + 1)}_${binary(array, idx + 0)}`; +} + export class LContainerDebug implements ILContainerDebug { constructor(private readonly _raw_lContainer: LContainer) {} diff --git a/packages/core/src/render3/interfaces/injector.ts b/packages/core/src/render3/interfaces/injector.ts index 4ec1f234d5..1c5da2b9a4 100644 --- a/packages/core/src/render3/interfaces/injector.ts +++ b/packages/core/src/render3/interfaces/injector.ts @@ -9,6 +9,7 @@ import {InjectionToken} from '../../di/injection_token'; import {InjectFlags} from '../../di/interface/injector'; import {Type} from '../../interface/type'; +import {assertDefined, assertEqual} from '../../util/assert'; import {TDirectiveHostNode} from './node'; import {LView, TData} from './view'; @@ -239,6 +240,8 @@ export class NodeInjectorFactory { isViewProvider: boolean, injectImplementation: null| ((token: Type|InjectionToken, flags?: InjectFlags) => T)) { + ngDevMode && assertDefined(factory, 'Factory not specified'); + ngDevMode && assertEqual(typeof factory, 'function', 'Expected factory function.'); this.canSeeViewProviders = isViewProvider; this.injectImpl = injectImplementation; } diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 0aec9344b3..2c96f100bb 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -1021,4 +1021,48 @@ export interface DebugNode { * Child nodes */ children: DebugNode[]; + + /** + * A list of Component/Directive types which need to be instantiated an this location. + */ + factories: Type[]; + + /** + * A list of Component/Directive instances which were instantiated an this location. + */ + instances: unknown[]; + + /** + * NodeInjector information. + */ + injector: NodeInjectorDebug; +} + +interface NodeInjectorDebug { + /** + * Instance bloom. Does the current injector have a provider with a given bloom mask. + */ + bloom: string; + + + /** + * Cumulative bloom. Do any of the above injectors have a provider with a given bloom mask. + */ + cumulativeBloom: string; + + /** + * A list of providers associated with this injector. + */ + providers: (Type|DirectiveDef|ComponentDef)[]; + + /** + * A list of providers associated with this injector visible to the view of the component only. + */ + viewProviders: Type[]; + + + /** + * Location of the parent `TNode`. + */ + parentInjectorIndex: number; } \ No newline at end of file diff --git a/packages/core/src/render3/util/discovery_utils.ts b/packages/core/src/render3/util/discovery_utils.ts index 94de7d03d5..1c66981fc8 100644 --- a/packages/core/src/render3/util/discovery_utils.ts +++ b/packages/core/src/render3/util/discovery_utils.ts @@ -389,7 +389,7 @@ export function getDebugNode(element: Element): DebugNode|null { // data. In this situation the TNode is not accessed at the same spot. const tNode = isLView(valueInLView) ? (valueInLView[T_HOST] as TNode) : getTNode(lView[TVIEW], nodeIndex - HEADER_OFFSET); - debugNode = buildDebugNode(tNode, lView, nodeIndex); + debugNode = buildDebugNode(tNode, lView); } return debugNode; diff --git a/packages/core/test/render3/instructions/lview_debug_spec.ts b/packages/core/test/render3/instructions/lview_debug_spec.ts index 785c631c78..bd35fa870e 100644 --- a/packages/core/test/render3/instructions/lview_debug_spec.ts +++ b/packages/core/test/render3/instructions/lview_debug_spec.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵProvidersFeature} from '@angular/core/src/core'; +import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart} from '@angular/core/src/render3/instructions/element'; import {TNodeDebug} from '@angular/core/src/render3/instructions/lview_debug'; import {createTNode, createTView} from '@angular/core/src/render3/instructions/shared'; import {TNodeType} from '@angular/core/src/render3/interfaces/node'; @@ -13,7 +15,7 @@ import {LView, TView, TViewType} from '@angular/core/src/render3/interfaces/view import {enterView, leaveView} from '@angular/core/src/render3/state'; import {insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list'; import {KeyValueArray} from '@angular/core/src/util/array_utils'; - +import {TemplateFixture} from '../render_util'; describe('lView_debug', () => { const mockFirstUpdatePassLView: LView = [null, {firstUpdatePass: true}] as any; @@ -151,4 +153,88 @@ describe('lView_debug', () => { }); }); }); + + describe('di', () => { + it('should show basic information', () => { + class DepA { + static ɵfac = () => new DepA(); + } + class DepB { + static ɵfac = () => new DepB(); + } + + const instances: any[] = []; + class MyComponent { + constructor(public depA: DepA, public depB: DepB) { + instances.push(this); + } + static ɵfac = () => new MyComponent(ɵɵdirectiveInject(DepA), ɵɵdirectiveInject(DepB)); + static ɵcmp = ɵɵdefineComponent({ + type: MyComponent, + selectors: [['my-comp']], + decls: 1, + vars: 0, + template: function() {}, + features: [ɵɵProvidersFeature( + [DepA, {provide: String, useValue: 'String'}], + [DepB, {provide: Number, useValue: 123}])] + }); + } + + let myChild!: MyChild; + class MyChild { + constructor() { + myChild = this; + } + static ɵfac = () => new MyChild(); + static ɵdir = ɵɵdefineDirective({ + type: MyChild, + selectors: [['my-child']], + }); + } + + + class MyDirective { + constructor(public myComp: MyComponent) { + instances.push(this); + } + static ɵfac = () => new MyDirective(ɵɵdirectiveInject(MyComponent)); + static ɵdir = ɵɵdefineDirective({ + type: MyDirective, + selectors: [['', 'my-dir', '']], + }); + } + + const fixture = new TemplateFixture( + () => { + ɵɵelementStart(0, 'my-comp', 0); + ɵɵelement(1, 'my-child'); + ɵɵelementEnd(); + }, + () => null, 2, 0, [MyComponent, MyDirective, MyChild], null, null, undefined, + [['my-dir', '']]); + const lView = fixture.hostView; + const lViewDebug = lView.debug!; + const myCompNode = lViewDebug.nodes[0]; + expect(myCompNode.factories).toEqual([MyComponent, MyDirective]); + expect(myCompNode.instances).toEqual(instances); + expect(myCompNode.injector).toEqual({ + bloom: jasmine.anything(), + cumulativeBloom: jasmine.anything(), + providers: [DepA, String, MyComponent.ɵcmp, MyDirective.ɵdir], + viewProviders: [DepB, Number], + parentInjectorIndex: -1, + }); + const myChildNode = myCompNode.children[0]; + expect(myChildNode.factories).toEqual([MyChild]); + expect(myChildNode.instances).toEqual([myChild]); + expect(myChildNode.injector).toEqual({ + bloom: jasmine.anything(), + cumulativeBloom: jasmine.anything(), + providers: [MyChild.ɵdir], + viewProviders: [], + parentInjectorIndex: 22, + }); + }); + }); });