Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/core/schematics/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ bundle_entrypoints = [
"safe-optional-chaining",
"packages/core/schematics/migrations/safe-optional-chaining/index.js",
],
[
"ng-template-unclaimed-bindings",
"packages/core/schematics/migrations/ng-template-unclaimed-bindings/index.js",
],
]

rollup.rollup(
Expand All @@ -170,6 +174,7 @@ rollup.rollup(
"//packages/core/schematics/migrations/http-xhr-backend",
"//packages/core/schematics/migrations/incremental-hydration",
"//packages/core/schematics/migrations/model-output",
"//packages/core/schematics/migrations/ng-template-unclaimed-bindings",
"//packages/core/schematics/migrations/safe-optional-chaining",
"//packages/core/schematics/migrations/strict-safe-navigation-narrow",
"//packages/core/schematics/migrations/strict-templates-default",
Expand Down
10 changes: 10 additions & 0 deletions packages/core/schematics/migrations.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{
"schematics": {
"ng-template-unclaimed-bindings": {
"version": "22.1.0",
"description": "Removes property bindings on '<ng-template>' elements that are not matched by any directive. Such bindings had no effect at runtime and are now reported as compile-time errors (NG8002).",
"factory": "./bundles/ng-template-unclaimed-bindings.cjs#migrate"
},
"change-detection-eager": {
"version": "22.0.0",
"description": "Adds `ChangeDetectionStrategy.Eager` to all components.",
Expand Down Expand Up @@ -39,6 +44,11 @@
"version": "22.0.0",
"description": "Wraps optional chaining expressions in $safeNavigationMigration().",
"factory": "./bundles/safe-optional-chaining.cjs#migrate"
},
"ng-template-unclaimed-bindings": {
"version": "22.1.0",
"description": "Removes property bindings on <ng-template> elements that are not claimed by any directive, which are now reported as compile errors.",
"factory": "./bundles/ng-template-unclaimed-bindings.cjs#migrate"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
load("//tools:defaults.bzl", "jasmine_test", "ts_project")

package(
default_visibility = [
"//packages/core/schematics:__pkg__",
"//packages/core/schematics/test:__pkg__",
],
)

ts_project(
name = "ng-template-unclaimed-bindings",
srcs = glob(
["**/*.ts"],
exclude = ["*.spec.ts"],
),
deps = [
"//:node_modules/@angular-devkit/schematics",
"//:node_modules/typescript",
"//packages/compiler-cli",
"//packages/core/schematics/utils",
"//packages/core/schematics/utils/tsurge",
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
],
)

ts_project(
name = "test_lib",
testonly = True,
srcs = glob(["*.spec.ts"]),
deps = [
":ng-template-unclaimed-bindings",
"//:node_modules/typescript",
"//packages/compiler-cli",
"//packages/core/schematics/utils/tsurge",
],
)

jasmine_test(
name = "test",
data = [":test_lib"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @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 {Rule} from '@angular-devkit/schematics';
import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
import {NgTemplateUnclaimedBindingsMigration} from './migration';

export function migrate(): Rule {
return async (tree) => {
await runMigrationInDevkit({
tree,
getMigration: () => new NgTemplateUnclaimedBindingsMigration(),
});
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* @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 {OptimizeFor} from '@angular/compiler-cli';
import ts from 'typescript';
import {
confirmAsSerializable,
ProgramInfo,
projectFile,
Replacement,
Serializable,
TextUpdate,
TsurgeFunnelMigration,
} from '../../utils/tsurge';

export interface CompilationUnitData {
replacements: Replacement[];
}

/**
* Error code of the "Can't bind to 'x' since it isn't a known property of 'y'" diagnostic
* produced by the template type checker (`ngErrorCode(ErrorCode.SCHEMA_INVALID_ATTRIBUTE)`,
* reported as `NG8002`).
*/
const UNKNOWN_PROPERTY_DIAGNOSTIC_CODE = -998002;

export class NgTemplateUnclaimedBindingsMigration extends TsurgeFunnelMigration<
CompilationUnitData,
CompilationUnitData
> {
override async analyze(info: ProgramInfo): Promise<Serializable<CompilationUnitData>> {
const replacements: Replacement[] = [];

if (info.ngCompiler === null) {
return confirmAsSerializable({replacements});
}

const seen = new Set<string>();

for (const sourceFile of info.sourceFiles) {
const diagnostics = info.ngCompiler.getDiagnosticsForFile(
sourceFile,
OptimizeFor.WholeProgram,
);

for (const diag of diagnostics) {
if (
diag.code !== UNKNOWN_PROPERTY_DIAGNOSTIC_CODE ||
diag.file === undefined ||
diag.start === undefined ||
diag.length === undefined
) {
continue;
}

const message = ts.flattenDiagnosticMessageText(diag.messageText, '\n');
if (!message.includes(`known property of 'ng-template'`)) {
continue;
}

const text = diag.file.text;
const end = diag.start + diag.length;

// Only remove actual property/two-way bindings (`[foo]="..."`, `[(foo)]="..."`).
if (text[diag.start] !== '[') {
continue;
}

// Include the whitespace between the binding and whatever precedes it.
let start = diag.start;
while (start > 0 && /\s/.test(text[start - 1])) {
start--;
}

// The same diagnostic can be reported through multiple source files, e.g. for
// external templates shared by more than one component.
const key = `${diag.file.fileName}@${start}:${end}`;
if (seen.has(key)) {
continue;
}
seen.add(key);

replacements.push(
new Replacement(
projectFile(diag.file, info),
new TextUpdate({position: start, end, toInsert: ''}),
),
);
}
}

return confirmAsSerializable({replacements});
}

override async combine(
unitA: CompilationUnitData,
unitB: CompilationUnitData,
): Promise<Serializable<CompilationUnitData>> {
const seen = new Set<string>();
const replacements: Replacement[] = [];

for (const replacement of [...unitA.replacements, ...unitB.replacements]) {
const {position, end, toInsert} = replacement.update.data;
const key = `${replacement.projectFile.rootRelativePath}@${position}:${end}:${toInsert}`;

if (!seen.has(key)) {
seen.add(key);
replacements.push(replacement);
}
}

return confirmAsSerializable({replacements});
}

override async globalMeta(
combinedData: CompilationUnitData,
): Promise<Serializable<CompilationUnitData>> {
return confirmAsSerializable(combinedData);
}

override async stats(globalMetadata: CompilationUnitData) {
return confirmAsSerializable({
removedBindings: globalMetadata.replacements.length,
});
}

override async migrate(globalData: CompilationUnitData) {
return {replacements: globalData.replacements};
}
}
Loading
Loading