diff --git a/packages/core/src/render3/debug.ts b/packages/core/src/render3/debug.ts index bc1d7982abf2c..9d721224d2d85 100644 --- a/packages/core/src/render3/debug.ts +++ b/packages/core/src/render3/debug.ts @@ -7,14 +7,18 @@ */ import {assertDefined} from '../util/assert'; - import {ACTIVE_INDEX, LContainer, NATIVE, VIEWS} from './interfaces/container'; +import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, TIcu} from './interfaces/i18n'; import {TNode} from './interfaces/node'; import {LQueries} from './interfaces/query'; import {RComment, RElement} from './interfaces/renderer'; import {StylingContext} from './interfaces/styling'; -import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TVIEW, TView, T_HOST} from './interfaces/view'; -import {unwrapRNode} from './util/view_utils'; +import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TVIEW, T_HOST} from './interfaces/view'; +import {getTNode, unwrapRNode} from './util/view_utils'; + +function attachDebugObject(obj: any, debug: any) { + Object.defineProperty(obj, 'debug', {value: debug, enumerable: false}); +} /* * This file contains conditionally attached classes which provide human readable (debug) level @@ -47,11 +51,11 @@ import {unwrapRNode} from './util/view_utils'; export function attachLViewDebug(lView: LView) { - (lView as any).debug = new LViewDebug(lView); + attachDebugObject(lView, new LViewDebug(lView)); } export function attachLContainerDebug(lContainer: LContainer) { - (lContainer as any).debug = new LContainerDebug(lContainer); + attachDebugObject(lContainer, new LContainerDebug(lContainer)); } export function toDebug(obj: LView): LViewDebug; @@ -230,3 +234,198 @@ export function readLViewValue(value: any): LView|null { } return null; } + +export class I18NDebugItem { + [key: string]: any; + + get tNode() { return getTNode(this.nodeIndex, this._lView); } + + constructor( + public __raw_opCode: any, private _lView: LView, public nodeIndex: number, + public type: string) {} +} + +/** + * Turns a list of "Create" & "Update" OpCodes into a human-readable list of operations for + * debugging purposes. + * @param mutateOpCodes mutation opCodes to read + * @param updateOpCodes update opCodes to read + * @param icus list of ICU expressions + * @param lView The view the opCodes are acting on + */ +export function attachI18nOpCodesDebug( + mutateOpCodes: I18nMutateOpCodes, updateOpCodes: I18nUpdateOpCodes, icus: TIcu[] | null, + lView: LView) { + attachDebugObject(mutateOpCodes, new I18nMutateOpCodesDebug(mutateOpCodes, lView)); + attachDebugObject(updateOpCodes, new I18nUpdateOpCodesDebug(updateOpCodes, icus, lView)); + + if (icus) { + icus.forEach(icu => { + icu.create.forEach( + icuCase => { attachDebugObject(icuCase, new I18nMutateOpCodesDebug(icuCase, lView)); }); + icu.update.forEach(icuCase => { + attachDebugObject(icuCase, new I18nUpdateOpCodesDebug(icuCase, icus, lView)); + }); + }); + } +} + +export class I18nMutateOpCodesDebug implements I18nOpCodesDebug { + constructor(private readonly __raw_opCodes: I18nMutateOpCodes, private readonly __lView: LView) {} + + /** + * A list of operation information about how the OpCodes will act on the view. + */ + get operations() { + const {__lView, __raw_opCodes} = this; + const results: any[] = []; + + for (let i = 0; i < __raw_opCodes.length; i++) { + const opCode = __raw_opCodes[i]; + let result: any; + if (typeof opCode === 'string') { + result = { + __raw_opCode: opCode, + type: 'Create Text Node', + nodeIndex: __raw_opCodes[++i], + text: opCode, + }; + } + + if (typeof opCode === 'number') { + switch (opCode & I18nMutateOpCode.MASK_OPCODE) { + case I18nMutateOpCode.AppendChild: + const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT; + result = new I18NDebugItem(opCode, __lView, destinationNodeIndex, 'AppendChild'); + break; + case I18nMutateOpCode.Select: + const nodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; + result = new I18NDebugItem(opCode, __lView, nodeIndex, 'Select'); + break; + case I18nMutateOpCode.ElementEnd: + let elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; + result = new I18NDebugItem(opCode, __lView, elementIndex, 'ElementEnd'); + break; + case I18nMutateOpCode.Attr: + elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; + result = new I18NDebugItem(opCode, __lView, elementIndex, 'Attr'); + result['attrName'] = __raw_opCodes[++i]; + result['attrValue'] = __raw_opCodes[++i]; + break; + } + } + + if (!result) { + switch (opCode) { + case COMMENT_MARKER: + result = { + __raw_opCode: opCode, + type: 'COMMENT_MARKER', + commentValue: __raw_opCodes[++i], + nodeIndex: __raw_opCodes[++i], + }; + break; + case ELEMENT_MARKER: + result = { + __raw_opCode: opCode, + type: 'ELEMENT_MARKER', + }; + break; + } + } + + if (!result) { + result = { + __raw_opCode: opCode, + type: 'Unknown Op Code', + code: opCode, + }; + } + + results.push(result); + } + + return results; + } +} + +export class I18nUpdateOpCodesDebug implements I18nOpCodesDebug { + constructor( + private readonly __raw_opCodes: I18nUpdateOpCodes, private readonly icus: TIcu[]|null, + private readonly __lView: LView) {} + + /** + * A list of operation information about how the OpCodes will act on the view. + */ + get operations() { + const {__lView, __raw_opCodes, icus} = this; + const results: any[] = []; + + for (let i = 0; i < __raw_opCodes.length; i++) { + // bit code to check if we should apply the next update + const checkBit = __raw_opCodes[i] as number; + // Number of opCodes to skip until next set of update codes + const skipCodes = __raw_opCodes[++i] as number; + let value = ''; + for (let j = i + 1; j <= (i + skipCodes); j++) { + const opCode = __raw_opCodes[j]; + if (typeof opCode === 'string') { + value += opCode; + } else if (typeof opCode == 'number') { + if (opCode < 0) { + // It's a binding index whose value is negative + // We cannot know the value of the binding so we only show the index + value += `�${-opCode - 1}�`; + } else { + const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF; + let tIcuIndex: number; + let tIcu: TIcu; + switch (opCode & I18nUpdateOpCode.MASK_OPCODE) { + case I18nUpdateOpCode.Attr: + const attrName = __raw_opCodes[++j] as string; + const sanitizeFn = __raw_opCodes[++j]; + results.push({ + __raw_opCode: opCode, + checkBit, + type: 'Attr', + attrValue: value, attrName, sanitizeFn, + }); + break; + case I18nUpdateOpCode.Text: + results.push({ + __raw_opCode: opCode, + checkBit, + type: 'Text', nodeIndex, + text: value, + }); + break; + case I18nUpdateOpCode.IcuSwitch: + tIcuIndex = __raw_opCodes[++j] as number; + tIcu = icus ![tIcuIndex]; + let result = new I18NDebugItem(opCode, __lView, nodeIndex, 'IcuSwitch'); + result['tIcuIndex'] = tIcuIndex; + result['checkBit'] = checkBit; + result['mainBinding'] = value; + result['tIcu'] = tIcu; + results.push(result); + break; + case I18nUpdateOpCode.IcuUpdate: + tIcuIndex = __raw_opCodes[++j] as number; + tIcu = icus ![tIcuIndex]; + result = new I18NDebugItem(opCode, __lView, nodeIndex, 'IcuUpdate'); + result['tIcuIndex'] = tIcuIndex; + result['checkBit'] = checkBit; + result['tIcu'] = tIcu; + results.push(result); + break; + } + } + } + } + i += skipCodes; + } + return results; + } +} + +export interface I18nOpCodesDebug { operations: any[]; } diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 846ed7c99f735..6dabda3d2dc67 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -13,6 +13,7 @@ import {addAllToArray} from '../util/array_utils'; import {assertDefined, assertEqual, assertGreaterThan} from '../util/assert'; import {attachPatchData} from './context_discovery'; +import {attachI18nOpCodesDebug} from './debug'; import {elementAttribute, load, textBinding} from './instructions/all'; import {allocExpando, createNodeAtIndex} from './instructions/shared'; import {LContainer, NATIVE} from './interfaces/container'; @@ -457,6 +458,10 @@ function i18nStartFirstPass( allocExpando(viewData, i18nVarsCount); + ngDevMode && + attachI18nOpCodesDebug( + createOpCodes, updateOpCodes, icuExpressions.length ? icuExpressions : null, viewData); + // NOTE: local var needed to properly assert the type of `TI18n`. const tI18n: TI18n = { vars: i18nVarsCount, @@ -464,6 +469,7 @@ function i18nStartFirstPass( update: updateOpCodes, icus: icuExpressions.length ? icuExpressions : null, }; + tView.data[index + HEADER_OFFSET] = tI18n; } diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index 2d68b094ecfc2..0ab15822b8c7e 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -16,7 +16,7 @@ import {AttributeMarker} from '../../src/render3/interfaces/node'; import {getNativeByIndex, getTNode} from '../../src/render3/util/view_utils'; import {NgForOf, NgIf} from './common_with_def'; import {allocHostVars, element, elementEnd, elementStart, template, text, nextContext, bind, elementProperty, projectionDef, projection, elementContainerStart, elementContainerEnd, textBinding} from '../../src/render3/instructions/all'; -import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, TI18n} from '../../src/render3/interfaces/i18n'; +import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, TI18n, IcuType} from '../../src/render3/interfaces/i18n'; import {HEADER_OFFSET, LView, TVIEW} from '../../src/render3/interfaces/view'; import {ComponentFixture, TemplateFixture} from './render_util'; @@ -81,6 +81,17 @@ describe('Runtime i18n', () => { const index = 0; const opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV); }, null, nbConsts, index); + + // Check debug + const debugOps = (opCodes as any).create.debug !.operations; + expect(debugOps[0].__raw_opCode).toBe('simple text'); + expect(debugOps[0].type).toBe('Create Text Node'); + expect(debugOps[0].nodeIndex).toBe(1); + expect(debugOps[0].text).toBe('simple text'); + expect(debugOps[1].__raw_opCode).toBe(1); + expect(debugOps[1].type).toBe('AppendChild'); + expect(debugOps[1].nodeIndex).toBe(0); + expect(opCodes).toEqual({ vars: 1, create: [ @@ -138,6 +149,10 @@ describe('Runtime i18n', () => { const index = 1; const opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV); }, null, nbConsts, index); + expect((opCodes as any).update.debug.operations).toEqual([ + {__raw_opCode: 8, checkBit: 1, type: 'Text', nodeIndex: 2, text: 'Hello �0�!'} + ]); + expect(opCodes).toEqual({ vars: 1, create: @@ -282,6 +297,79 @@ describe('Runtime i18n', () => { const innerTextNode = index + 4; const lastTextNode = index + 5; + const debugOps = (opCodes as any).update.debug.operations; + expect(debugOps[0].__raw_opCode).toBe(6); + expect(debugOps[0].checkBit).toBe(1); + expect(debugOps[0].type).toBe('IcuSwitch'); + expect(debugOps[0].nodeIndex).toBe(1); + expect(debugOps[0].tIcuIndex).toBe(0); + expect(debugOps[0].mainBinding).toBe('�0�'); + + expect(debugOps[1].__raw_opCode).toBe(7); + expect(debugOps[1].checkBit).toBe(3); + expect(debugOps[1].type).toBe('IcuUpdate'); + expect(debugOps[1].nodeIndex).toBe(1); + expect(debugOps[1].tIcuIndex).toBe(0); + + const icuDebugOps = (opCodes as any).icus[0].create[0].debug.operations; + let op: any; + let i = 0; + + op = icuDebugOps[i++]; + expect(op.__raw_opCode).toBe('no '); + expect(op.type).toBe('Create Text Node'); + expect(op.nodeIndex).toBe(2); + expect(op.text).toBe('no '); + + op = icuDebugOps[i++]; + expect(op.__raw_opCode).toBe(131073); + expect(op.type).toBe('AppendChild'); + expect(op.nodeIndex).toBe(1); + + op = icuDebugOps[i++]; + expect(op.__raw_opCode).toEqual({marker: 'element'}); + expect(op.type).toBe('ELEMENT_MARKER'); + + op = icuDebugOps[i++]; + expect(op.__raw_opCode).toBe('b'); + expect(op.type).toBe('Create Text Node'); + expect(op.nodeIndex).toBe(3); + expect(op.text).toBe('b'); + + op = icuDebugOps[i++]; + expect(op.__raw_opCode).toBe(131073); + expect(op.type).toBe('AppendChild'); + expect(op.nodeIndex).toBe(1); + + op = icuDebugOps[i++]; + expect(op.__raw_opCode).toBe(28); + expect(op.type).toBe('Attr'); + expect(op.nodeIndex).toBe(3); + expect(op.attrName).toBe('title'); + expect(op.attrValue).toBe('none'); + + op = icuDebugOps[i++]; + expect(op.__raw_opCode).toBe('emails'); + expect(op.type).toBe('Create Text Node'); + expect(op.nodeIndex).toBe(4); + expect(op.text).toBe('emails'); + + op = icuDebugOps[i++]; + expect(op.__raw_opCode).toBe(393217); + expect(op.type).toBe('AppendChild'); + expect(op.nodeIndex).toBe(3); + + op = icuDebugOps[i++]; + expect(op.__raw_opCode).toBe('!'); + expect(op.type).toBe('Create Text Node'); + expect(op.nodeIndex).toBe(5); + expect(op.text).toBe('!'); + + op = icuDebugOps[i++]; + expect(op.__raw_opCode).toBe(131073); + expect(op.type).toBe('AppendChild'); + expect(op.nodeIndex).toBe(1); + expect(opCodes).toEqual({ vars: 5, create: [ @@ -2168,5 +2256,4 @@ describe('Runtime i18n', () => { }); }); }); - });