mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(migrations): skip migration for inputs with 'this' references (#64142)
Prevents migration of @input() properties that contain references to 'this' in their initializer functions. This ensures that functions accessing class members via 'this' remain unchanged, preventing potential build errors. PR Close #64142
This commit is contained in:
+70
-1
@@ -41,6 +41,15 @@ export function convertToSignalInput(
|
||||
importManager: ImportManager,
|
||||
result: MigrationResult,
|
||||
): Replacement[] {
|
||||
// Check for 'this' references in initializer before doing anything else
|
||||
if (
|
||||
node.initializer &&
|
||||
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
|
||||
containsThisReferences(node.initializer)
|
||||
) {
|
||||
return []; // Skip migration for this input by returning empty replacements
|
||||
}
|
||||
|
||||
let optionsLiteral: ts.ObjectLiteralExpression | null = null;
|
||||
|
||||
// We need an options array for the input because:
|
||||
@@ -137,12 +146,31 @@ export function convertToSignalInput(
|
||||
modifiersWithoutInputDecorator.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword));
|
||||
}
|
||||
|
||||
// Skip migration if the input is a function that references class members via 'this'
|
||||
if (
|
||||
inputInitializer &&
|
||||
(ts.isArrowFunction(inputInitializer) || ts.isFunctionExpression(inputInitializer))
|
||||
) {
|
||||
if (containsThisReferences(inputInitializer)) {
|
||||
return []; // Skip migration for this input by returning empty replacements
|
||||
}
|
||||
}
|
||||
|
||||
let finalInitializer: ts.Expression | undefined = inputInitializer;
|
||||
if (inputInitializer === undefined) {
|
||||
if (preferShorthandIfPossible === null) {
|
||||
finalInitializer = ts.factory.createIdentifier('undefined');
|
||||
} else {
|
||||
resolvedType = preferShorthandIfPossible.originalType;
|
||||
}
|
||||
}
|
||||
|
||||
const newNode = ts.factory.createPropertyDeclaration(
|
||||
modifiersWithoutInputDecorator,
|
||||
node.name,
|
||||
undefined,
|
||||
undefined,
|
||||
inputInitializer,
|
||||
finalInitializer,
|
||||
);
|
||||
|
||||
const newPropertyText = result.printer.printNode(
|
||||
@@ -222,3 +250,44 @@ function extractTransformOfInput(
|
||||
leadingTodoText,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a function node contains any references to 'this'.
|
||||
* This is used to skip migration for functions that reference class members.
|
||||
*/
|
||||
function containsThisReferences(node: ts.Node): boolean {
|
||||
let hasThis = false;
|
||||
|
||||
const visit = (node: ts.Node) => {
|
||||
if (hasThis) return;
|
||||
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
hasThis = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ts.isPropertyAccessExpression(node)) {
|
||||
const expr = node.expression;
|
||||
if (ts.isIdentifier(expr) && expr.text === 'this') {
|
||||
hasThis = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
|
||||
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
||||
if (node.body) {
|
||||
if (node.body.kind === ts.SyntaxKind.Block) {
|
||||
(node.body as ts.Block).statements.forEach(visit);
|
||||
} else {
|
||||
visit(node.body);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
return hasThis;
|
||||
}
|
||||
|
||||
@@ -128,6 +128,30 @@ describe('signal input migration', () => {
|
||||
expect(messages).toContain(` -> Migrated 1/2 inputs.`);
|
||||
});
|
||||
|
||||
it('should not migrate input functions that reference this', async () => {
|
||||
const source = `
|
||||
import {Component, Input} from '@angular/core';
|
||||
|
||||
@Component({})
|
||||
export class TestComponent {
|
||||
|
||||
@Input() fnDisplay = (item: any) => item;
|
||||
@Input() fnEquals = (a: any, b: any) => this.fnDisplay(a) === this.fnDisplay(b);
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
writeFile('/index.ts', source);
|
||||
await runMigration();
|
||||
const content = tree.readContent('/index.ts');
|
||||
|
||||
// Verify the simple function was migrated
|
||||
expect(content).toContain('readonly fnDisplay = input((item: any) => item);');
|
||||
expect(content).toContain(
|
||||
'@Input() fnEquals = (a: any, b: any) => this.fnDisplay()(a) === this.fnDisplay()(b);',
|
||||
);
|
||||
});
|
||||
|
||||
it('should report correct statistics with best effort mode', async () => {
|
||||
writeFile(`node_modules/@tsconfig/strictest/tsconfig.json`, `{}`);
|
||||
writeFile(
|
||||
|
||||
Reference in New Issue
Block a user