diff --git a/packages/core/schematics/migrations/signal-migration/src/convert-input/convert_to_signal.ts b/packages/core/schematics/migrations/signal-migration/src/convert-input/convert_to_signal.ts index efe436eb685..ce6c2dbc605 100644 --- a/packages/core/schematics/migrations/signal-migration/src/convert-input/convert_to_signal.ts +++ b/packages/core/schematics/migrations/signal-migration/src/convert-input/convert_to_signal.ts @@ -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; +} diff --git a/packages/core/schematics/test/signal_input_migration_spec.ts b/packages/core/schematics/test/signal_input_migration_spec.ts index f397cb271c3..602f308832e 100644 --- a/packages/core/schematics/test/signal_input_migration_spec.ts +++ b/packages/core/schematics/test/signal_input_migration_spec.ts @@ -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(