fix(migrations): migrating input with more than 1 usage in a method (#64367)

When the migration command was run for inputs, if the input had more than one reference in a method the migration would generate incorrect code

Fixes #63018

PR Close #64367
This commit is contained in:
aparziale 2025-10-13 12:24:09 +02:00 committed by Andrew Kushnir
parent 4b965f5b77
commit e99bbd3d2a
4 changed files with 100 additions and 1 deletions

View file

@ -11,7 +11,7 @@ import {analyzeControlFlow, ControlFlowAnalysisNode} from '../../../flow_analysi
import {ProgramInfo, projectFile, Replacement, TextUpdate} from '../../../../../../utils/tsurge';
import {traverseAccess} from '../../../utils/traverse_access';
import {UniqueNamesGenerator} from '../../../utils/unique_names';
import {createNewBlockToInsertVariable} from '../helpers/create_block_arrow_function';
import {createNewBlockToInsertVariable} from './create_block_arrow_function';
import assert from 'assert';
export interface NarrowableTsReferences {
@ -123,6 +123,23 @@ export function migrateStandardTsReference(
replacements.push(
...createNewBlockToInsertVariable(parent, filePath, temporaryVariableStr),
);
} else if (shouldInsertAtMethodStart(reference, recommendedNode, referenceNodeInBlock)) {
const blockNode = recommendedNode as ts.Block;
const firstStatement = blockNode.statements[0];
const leadingSpace = firstStatement
? ts.getLineAndCharacterOfPosition(sf, firstStatement.getStart())
: ts.getLineAndCharacterOfPosition(sf, referenceNodeInBlock.getStart());
replacements.push(
new Replacement(
filePath,
new TextUpdate({
position: firstStatement.getStart(),
end: firstStatement.getStart(),
toInsert: `${temporaryVariableStr}\n${' '.repeat(leadingSpace.character)}`,
}),
),
);
} else {
const leadingSpace = ts.getLineAndCharacterOfPosition(sf, referenceNodeInBlock.getStart());
@ -151,3 +168,43 @@ export function migrateStandardTsReference(
}
}
}
/**
* Determines if a temporary variable should be inserted at the start of a method.
*
* This function performs several checks to ensure it's safe to insert a temporary variable:
* 1. Verifies the recommended node is a method declaration block
* 2. Ensures all references are contained within the method body
* 3. Confirms the reference node is the first statement in the method
* 4. Validates the reference node is an expression statement with an assignment operation
*/
function shouldInsertAtMethodStart(
references: NarrowableTsReferences,
recommendedNode: ts.Node,
referenceNodeInBlock: ts.Node,
): boolean {
if (!ts.isBlock(recommendedNode) || !ts.isMethodDeclaration(recommendedNode.parent)) {
return false;
}
const methodBody = recommendedNode;
const allReferencesInMethod = references.accesses.every((access) => {
let current: ts.Node | undefined = access;
while (current && current !== methodBody) {
current = current.parent;
}
return current === methodBody;
});
if (!allReferencesInMethod) {
return false;
}
return (
methodBody.statements.length > 0 &&
ts.isExpressionStatement(referenceNodeInBlock) &&
methodBody.statements[0] === referenceNodeInBlock &&
ts.isBinaryExpression(referenceNodeInBlock.expression) &&
referenceNodeInBlock.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken
);
}

View file

@ -0,0 +1,12 @@
// tslint:disable
import {Input} from '@angular/core';
export class TestMigrationComponent {
@Input() public model: any;
public onSaveClick(): void {
this.model.requisitionId = 145;
this.model.comment = 'value';
}
}

View file

@ -848,6 +848,21 @@ class ModifierScenarios {
@CustomDecorator()
protected readonly usingCustomDecorator = input(true);
}
@@@@@@ multiple_references_in_method.ts @@@@@@
// tslint:disable
import {input} from '@angular/core';
export class TestMigrationComponent {
public readonly model = input<any>();
public onSaveClick(): void {
const model = this.model();
model.requisitionId = 145;
model.comment = 'value';
}
}
@@@@@@ mutate.ts @@@@@@
// tslint:disable

View file

@ -815,6 +815,21 @@ class ModifierScenarios {
@CustomDecorator()
protected readonly usingCustomDecorator = input(true);
}
@@@@@@ multiple_references_in_method.ts @@@@@@
// tslint:disable
import {input} from '@angular/core';
export class TestMigrationComponent {
public readonly model = input<any>();
public onSaveClick(): void {
const model = this.model();
model.requisitionId = 145;
model.comment = 'value';
}
}
@@@@@@ mutate.ts @@@@@@
// tslint:disable