Skip to content

Commit

Permalink
refactor(compiler): generate debug location instruction
Browse files Browse the repository at this point in the history
Adds the logic that will generate the `ɵɵattachSourceLocations` instruction.

Fixes angular#42530.
  • Loading branch information
crisbeto committed Nov 30, 2024
1 parent d287104 commit 5b87145
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression>
: ChangeDetectionStrategy.Default,
animations: metaObj.has('animations') ? metaObj.getOpaque('animations') : null,
relativeContextFilePath: this.sourceUrl,
relativeTemplatePath: null,
i18nUseExternalIds: false,
declarations,
};
Expand Down
140 changes: 140 additions & 0 deletions packages/compiler-cli/test/ngtsc/attach_source_location_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing';
import {NgtscTestEnvironment} from './env';
import {setEnableTemplateSourceLocations} from '@angular/compiler/src/render3/view/config';

const testFiles = loadStandardTestFiles({fakeCommon: true});

runInEachFileSystem(() => {
describe('source location instruction generation', () => {
let env!: NgtscTestEnvironment;

beforeEach(() => {
setEnableTemplateSourceLocations(true);
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig();
});

afterEach(() => {
setEnableTemplateSourceLocations(false);
});

it('should attach the source location in an inline template', () => {
env.write(
`test.ts`,
`
import {Component} from '@angular/core';
@Component({
template: \`
<div><span>
<strong>Hello</strong>
</span></div>
\`,
})
class Comp {}
`,
);
env.driveMain();
const content = env.getContents('test.js');
expect(content).toContain('ɵɵelementStart(0, "div")(1, "span")(2, "strong");');
expect(content).toContain(
'ɵɵattachSourceLocations("test.ts", [[0, 114, 5, 14], [1, 119, 5, 19], [2, 142, 6, 16]]);',
);
});

it('should attach the source location in an external template', () => {
env.write(
'test.html',
`
<div><span>
<strong>Hello</strong>
</span></div>
`,
);

env.write(
`test.ts`,
`
import {Component} from '@angular/core';
@Component({templateUrl: './test.html'})
class Comp {}
`,
);
env.driveMain();
const content = env.getContents('test.js');
expect(content).toContain('ɵɵelementStart(0, "div")(1, "span")(2, "strong");');
expect(content).toContain(
'ɵɵattachSourceLocations("test.html", [[0, 9, 1, 8], [1, 14, 1, 13], [2, 31, 2, 10]]);',
);
});

it('should attach the source location to structural directives', () => {
env.write(
`test.ts`,
`
import {Component} from '@angular/core';
import {CommonModule} from '@angular/common';
@Component({
imports: [CommonModule],
template: \`
<div *ngIf="true">
<span></span>
</div>
\`,
})
class Comp {}
`,
);
env.driveMain();
const content = env.getContents('test.js');
expect(content).toContain('ɵɵtemplate(0,');
expect(content).toContain('ɵɵelementStart(0, "div");');
expect(content).toContain('ɵɵelement(1, "span");');
expect(content).toContain(
'ɵɵattachSourceLocations("test.ts", [[0, 207, 7, 14], [1, 242, 8, 16]]);',
);
});

it('should not attach the source location to ng-container', () => {
env.write(
`test.ts`,
`
import {Component} from '@angular/core';
@Component({
template: \`
<ng-container>
<div>
<ng-container>
<span></span>
</ng-container>
</div>
</ng-container>
\`,
})
class Comp {}
`,
);
env.driveMain();
const content = env.getContents('test.js');
expect(content).toContain('ɵɵelementContainerStart(0);');
expect(content).toContain('ɵɵelementStart(1, "div");');
expect(content).toContain('ɵɵelementContainerStart(2);');
expect(content).toContain('ɵɵelement(3, "span");');
expect(content).toContain(
'ɵɵattachSourceLocations("test.ts", [[1, 145, 6, 16], [3, 204, 8, 20]]);',
);
});
});
});
4 changes: 2 additions & 2 deletions packages/compiler/src/render3/view/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ import {
DeclarationListEmitMode,
DeferBlockDepsEmitMode,
R3ComponentMetadata,
R3DeferPerBlockDependency,
R3DeferPerComponentDependency,
R3DeferResolverFunctionMetadata,
R3DirectiveMetadata,
R3HostMetadata,
R3TemplateDependency,
} from './api';
import {ENABLE_TEMPLATE_SOURCE_LOCATIONS} from './config';
import {createContentQueriesFunction, createViewQueriesFunction} from './query_generation';
import {makeBindingParser} from './template';
import {asLiteral, conditionallyCreateDirectiveBindingLiteral, DefinitionMap} from './util';
Expand Down Expand Up @@ -235,6 +234,7 @@ export function compileComponentFromMetadata(
meta.defer,
allDeferrableDepsFn,
meta.relativeTemplatePath,
ENABLE_TEMPLATE_SOURCE_LOCATIONS,
);

// Then the IR is transformed to prepare it for cod egeneration.
Expand Down
24 changes: 24 additions & 0 deletions packages/compiler/src/render3/view/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

/**
* Whether to produce instructions that will attach the source location to each DOM node.
*
* !!!Important!!! at the time of writing this flag isn't exposed externally, but internal debug
* tools enable it via a local change. Any modifications to this flag need to update the
* internal tooling as well.
*/
export let ENABLE_TEMPLATE_SOURCE_LOCATIONS = false;

/**
* Utility function to set the `ENABLE_TEMPLATE_SOURCE_LOCATIONS` flag.
* Intended to be used **only** inside unit tests.
*/
export function setEnableTemplateSourceLocations(value: boolean): void {
ENABLE_TEMPLATE_SOURCE_LOCATIONS = value;
}
5 changes: 5 additions & 0 deletions packages/compiler/src/template/pipeline/ir/src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ export enum OpKind {
* A creation op that corresponds to i18n attributes on an element.
*/
I18nAttributes,

/**
* Creation op that attaches the location at which an element was defined in a template to it.
*/
SourceLocation,
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,7 @@ export function transformExpressionsInOp(
case OpKind.I18nAttributes:
case OpKind.IcuPlaceholder:
case OpKind.DeclareLet:
case OpKind.SourceLocation:
// These operations contain no expressions.
break;
default:
Expand Down
33 changes: 32 additions & 1 deletion packages/compiler/src/template/pipeline/ir/src/ops/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ export type CreateOp =
| IcuPlaceholderOp
| I18nContextOp
| I18nAttributesOp
| DeclareLetOp;
| DeclareLetOp
| SourceLocationOp;

/**
* An operation representing the creation of an element or container.
Expand Down Expand Up @@ -1553,6 +1554,36 @@ export function createI18nAttributesOp(
};
}

/** Describes a location at which an element is defined within a template. */
export interface ElementSourceLocation {
targetSlot: SlotHandle;
offset: number;
line: number;
column: number;
}

/**
* Op that attaches the location at which each element is defined within the source template.
*/
export interface SourceLocationOp extends Op<CreateOp> {
kind: OpKind.SourceLocation;
templatePath: string;
locations: ElementSourceLocation[];
}

/** Create a `SourceLocationOp`. */
export function createSourceLocationOp(
templatePath: string,
locations: ElementSourceLocation[],
): SourceLocationOp {
return {
kind: OpKind.SourceLocation,
templatePath,
locations,
...NEW_OP,
};
}

/**
* An index into the `consts` array which is shared across the compilation of all views in a
* component.
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/template/pipeline/src/compilation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export class ComponentCompilationJob extends CompilationJob {
readonly deferMeta: R3ComponentDeferMetadata,
readonly allDeferrableDepsFn: o.ReadVarExpr | null,
readonly relativeTemplatePath: string | null,
readonly enableDebugLocations: boolean,
) {
super(componentName, pool, compatibility);
this.root = new ViewCompilationUnit(this, this.allocateXrefId(), null);
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/template/pipeline/src/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {wrapI18nIcus} from './phases/wrap_icus';
import {optimizeStoreLet} from './phases/store_let_optimization';
import {removeIllegalLetReferences} from './phases/remove_illegal_let_references';
import {generateLocalLetReferences} from './phases/generate_local_let_references';
import {attachSourceLocations} from './phases/attach_source_locations';

type Phase =
| {
Expand Down Expand Up @@ -159,6 +160,7 @@ const phases: Phase[] = [
{kind: Kind.Tmpl, fn: mergeNextContextExpressions},
{kind: Kind.Tmpl, fn: generateNgContainerOps},
{kind: Kind.Tmpl, fn: collapseEmptyInstructions},
{kind: Kind.Tmpl, fn: attachSourceLocations},
{kind: Kind.Tmpl, fn: disableBindings},
{kind: Kind.Both, fn: extractPureFunctions},
{kind: Kind.Both, fn: reify},
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/template/pipeline/src/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function ingestComponent(
deferMeta: R3ComponentDeferMetadata,
allDeferrableDepsFn: o.ReadVarExpr | null,
relativeTemplatePath: string | null,
enableDebugLocations: boolean,
): ComponentCompilationJob {
const job = new ComponentCompilationJob(
componentName,
Expand All @@ -69,6 +70,7 @@ export function ingestComponent(
deferMeta,
allDeferrableDepsFn,
relativeTemplatePath,
enableDebugLocations,
);
ingestNodes(job.root, template);
return job;
Expand Down
7 changes: 7 additions & 0 deletions packages/compiler/src/template/pipeline/src/instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,13 @@ export function pureFunction(
);
}

export function attachSourceLocation(
templatePath: string,
locations: o.LiteralArrayExpr,
): ir.CreateOp {
return call(Identifiers.attachSourceLocations, [o.literal(templatePath), locations], null);
}

/**
* Collates the string an expression arguments for an interpolation instruction.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import * as ir from '../../ir';
import type {ComponentCompilationJob} from '../compilation';

/**
* Locates all of the elements defined in a creation block and outputs an op
* that will expose their definition location in the DOM.
*/
export function attachSourceLocations(job: ComponentCompilationJob): void {
if (!job.enableDebugLocations || job.relativeTemplatePath === null) {
return;
}

for (const unit of job.units) {
const locations: ir.ElementSourceLocation[] = [];

for (const op of unit.create) {
if (op.kind === ir.OpKind.ElementStart || op.kind === ir.OpKind.Element) {
const start = op.startSourceSpan.start;
locations.push({
targetSlot: op.handle,
offset: start.offset,
line: start.line,
column: start.col,
});
}
}

if (locations.length > 0) {
unit.create.push(ir.createSourceLocationOp(job.relativeTemplatePath, locations));
}
}
}
17 changes: 17 additions & 0 deletions packages/compiler/src/template/pipeline/src/phases/reify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,23 @@ function reifyCreateOperations(unit: CompilationUnit, ops: ir.OpList<ir.CreateOp
),
);
break;
case ir.OpKind.SourceLocation:
const locationsLiteral = o.literalArr(
op.locations.map(({targetSlot, offset, line, column}) => {
if (targetSlot.slot === null) {
throw new Error('No slot was assigned for source location');
}
return o.literalArr([
o.literal(targetSlot.slot),
o.literal(offset),
o.literal(line),
o.literal(column),
]);
}),
);

ir.OpList.replace(op, ng.attachSourceLocation(op.templatePath, locationsLiteral));
break;
case ir.OpKind.Statement:
// Pass statement operations directly through.
break;
Expand Down

0 comments on commit 5b87145

Please sign in to comment.