Skip to content

Commit dc64f3e

Browse files
aparzimmalerba
authored andcommitted
fix(core): Fixed inject migration schematics for migrate destructured properties (#62832)
Fixes #62626 - Properties used with the destructor are also managed during migration. PR Close #62832
1 parent 957b367 commit dc64f3e

File tree

2 files changed

+272
-53
lines changed

2 files changed

+272
-53
lines changed

packages/core/schematics/ng-generate/inject-migration/migration.ts

Lines changed: 241 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,23 @@ function migrateClass(
280280
}
281281
}
282282

283+
interface ParameterMigrationContext<T extends ts.Node = ts.ParameterDeclaration> {
284+
node: T;
285+
options: MigrationOptions;
286+
localTypeChecker: ts.TypeChecker;
287+
printer: ts.Printer;
288+
tracker: ChangeTracker;
289+
superCall: ts.CallExpression | null;
290+
usedInSuper: boolean;
291+
usedInConstructor: boolean;
292+
usesOtherParams: boolean;
293+
memberIndentation: string;
294+
innerIndentation: string;
295+
prependToConstructor: string[];
296+
propsToAdd: string[];
297+
afterSuper: string[];
298+
}
299+
283300
/**
284301
* Migrates a single parameter to `inject()` DI.
285302
* @param node Parameter to be migrated.
@@ -312,11 +329,36 @@ function migrateParameter(
312329
propsToAdd: string[],
313330
afterSuper: string[],
314331
): void {
315-
if (!ts.isIdentifier(node.name)) {
332+
const context: ParameterMigrationContext = {
333+
node,
334+
options,
335+
localTypeChecker,
336+
printer,
337+
tracker,
338+
superCall,
339+
usedInSuper,
340+
usedInConstructor,
341+
usesOtherParams,
342+
memberIndentation,
343+
innerIndentation,
344+
prependToConstructor,
345+
propsToAdd,
346+
afterSuper,
347+
};
348+
349+
if (ts.isIdentifier(node.name)) {
350+
migrateIdentifierParameter(context, node.name);
351+
} else if (ts.isObjectBindingPattern(node.name)) {
352+
migrateObjectBindingParameter(context, node.name);
353+
} else {
316354
return;
317355
}
356+
}
357+
358+
function migrateIdentifierParameter(context: ParameterMigrationContext, name: ts.Identifier): void {
359+
const {node, options, localTypeChecker, printer, tracker, usedInConstructor, usesOtherParams} =
360+
context;
318361

319-
const name = node.name.text;
320362
const replacementCall = createInjectReplacementCall(
321363
node,
322364
options,
@@ -328,69 +370,215 @@ function migrateParameter(
328370

329371
// If the parameter declares a property, we need to declare it (e.g. `private foo: Foo`).
330372
if (declaresProp) {
331-
// We can't initialize the property if it's referenced within a `super` call or it references
332-
// other parameters. See the logic further below for the initialization.
333-
const canInitialize = !usedInSuper && !usesOtherParams;
334-
const prop = ts.factory.createPropertyDeclaration(
335-
cloneModifiers(
336-
node.modifiers?.filter((modifier) => {
337-
// Strip out the DI decorators, as well as `public` which is redundant.
338-
return !ts.isDecorator(modifier) && modifier.kind !== ts.SyntaxKind.PublicKeyword;
339-
}),
340-
),
341-
name,
342-
// Don't add the question token to private properties since it won't affect interface implementation.
343-
node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword)
344-
? undefined
345-
: node.questionToken,
346-
canInitialize ? undefined : node.type,
347-
canInitialize ? ts.factory.createIdentifier(PLACEHOLDER) : undefined,
348-
);
349-
350-
propsToAdd.push(
351-
memberIndentation +
352-
replaceNodePlaceholder(node.getSourceFile(), prop, replacementCall, printer),
353-
);
373+
handlePropertyDeclaration(context, name, replacementCall);
354374
}
355375

356376
// If the parameter is referenced within the constructor, we need to declare it as a variable.
357377
if (usedInConstructor) {
358-
if (usedInSuper) {
359-
// Usages of `this` aren't allowed before `super` calls so we need to
360-
// create a variable which calls `inject()` directly instead...
361-
prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
362-
363-
// ...then we can initialize the property after the `super` call.
364-
if (declaresProp) {
365-
afterSuper.push(`${innerIndentation}this.${name} = ${name};`);
366-
}
367-
} else if (declaresProp) {
368-
// If the parameter declares a property (`private foo: foo`) and is used inside the class
369-
// at the same time, we need to ensure that it's initialized to the value from the variable
370-
// and that we only reference `this` after the `super` call.
371-
const initializer = `${innerIndentation}const ${name} = this.${name};`;
372-
373-
if (superCall === null) {
374-
prependToConstructor.push(initializer);
375-
} else {
376-
afterSuper.push(initializer);
377-
}
378-
} else {
379-
// If the parameter is only referenced in the constructor, we
380-
// don't need to declare any new properties.
381-
prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
382-
}
378+
handleConstructorUsage(context, name.text, replacementCall, declaresProp);
383379
} else if (usesOtherParams && declaresProp) {
384-
const toAdd = `${innerIndentation}this.${name} = ${replacementCall};`;
380+
handleParameterWithDependencies(context, name.text, replacementCall);
381+
}
382+
}
383+
384+
function handlePropertyDeclaration(
385+
context: ParameterMigrationContext,
386+
name: ts.Identifier,
387+
replacementCall: string,
388+
): void {
389+
const {node, memberIndentation, propsToAdd} = context;
390+
391+
const canInitialize = !context.usedInSuper && !context.usesOtherParams;
392+
const prop = ts.factory.createPropertyDeclaration(
393+
cloneModifiers(
394+
node.modifiers?.filter((modifier) => {
395+
return !ts.isDecorator(modifier) && modifier.kind !== ts.SyntaxKind.PublicKeyword;
396+
}),
397+
),
398+
name,
399+
node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword)
400+
? undefined
401+
: node.questionToken,
402+
canInitialize ? undefined : node.type,
403+
canInitialize ? ts.factory.createIdentifier(PLACEHOLDER) : undefined,
404+
);
405+
406+
propsToAdd.push(
407+
memberIndentation +
408+
replaceNodePlaceholder(node.getSourceFile(), prop, replacementCall, context.printer),
409+
);
410+
}
411+
412+
function handleConstructorUsage(
413+
context: ParameterMigrationContext,
414+
name: string,
415+
replacementCall: string,
416+
declaresProp: boolean,
417+
): void {
418+
const {innerIndentation, prependToConstructor, afterSuper, superCall} = context;
419+
420+
if (context.usedInSuper) {
421+
// Usages of `this` aren't allowed before `super` calls so we need to
422+
// create a variable which calls `inject()` directly instead...
423+
prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
424+
425+
if (declaresProp) {
426+
afterSuper.push(`${innerIndentation}this.${name} = ${name};`);
427+
}
428+
} else if (declaresProp) {
429+
// If the parameter declares a property (`private foo: foo`) and is used inside the class
430+
// at the same time, we need to ensure that it's initialized to the value from the variable
431+
// and that we only reference `this` after the `super` call.
432+
const initializer = `${innerIndentation}const ${name} = this.${name};`;
385433

386434
if (superCall === null) {
387-
prependToConstructor.push(toAdd);
435+
prependToConstructor.push(initializer);
388436
} else {
389-
afterSuper.push(toAdd);
437+
afterSuper.push(initializer);
438+
}
439+
} else {
440+
// If the parameter is only referenced in the constructor, we
441+
// don't need to declare any new properties.
442+
prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
443+
}
444+
}
445+
446+
function handleParameterWithDependencies(
447+
context: ParameterMigrationContext,
448+
name: string,
449+
replacementCall: string,
450+
): void {
451+
const {innerIndentation, prependToConstructor, afterSuper, superCall} = context;
452+
453+
const toAdd = `${innerIndentation}this.${name} = ${replacementCall};`;
454+
455+
if (superCall === null) {
456+
prependToConstructor.push(toAdd);
457+
} else {
458+
afterSuper.push(toAdd);
459+
}
460+
}
461+
462+
function migrateObjectBindingParameter(
463+
context: ParameterMigrationContext,
464+
bindingPattern: ts.ObjectBindingPattern,
465+
): void {
466+
const {node, options, localTypeChecker, printer, tracker} = context;
467+
468+
const replacementCall = createInjectReplacementCall(
469+
node,
470+
options,
471+
localTypeChecker,
472+
printer,
473+
tracker,
474+
);
475+
476+
for (const element of bindingPattern.elements) {
477+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
478+
migrateBindingElement(context, element, element.name, replacementCall);
390479
}
391480
}
392481
}
393482

483+
function migrateBindingElement(
484+
context: ParameterMigrationContext,
485+
element: ts.BindingElement,
486+
elementName: ts.Identifier,
487+
replacementCall: string,
488+
): void {
489+
const propertyName = elementName.text;
490+
491+
// Determines how to access the property
492+
const propertyAccess = element.propertyName
493+
? `${replacementCall}.${element.propertyName.getText()}`
494+
: `${replacementCall}.${propertyName}`;
495+
496+
createPropertyForBindingElement(context, propertyName, propertyAccess);
497+
498+
if (context.usedInConstructor) {
499+
handleConstructorUsageBindingElement(context, element, propertyName);
500+
}
501+
}
502+
503+
function handleConstructorUsageBindingElement(
504+
context: ParameterMigrationContext,
505+
element: ts.BindingElement,
506+
propertyName: string,
507+
): void {
508+
const {tracker, localTypeChecker, node: paramNode} = context;
509+
const constructorDecl = paramNode.parent;
510+
511+
// Check in constructor or exist body content
512+
if (!ts.isConstructorDeclaration(constructorDecl) || !constructorDecl.body) {
513+
return;
514+
}
515+
516+
// Get the unique "symbol" for our unstructured property.
517+
const symbol = localTypeChecker.getSymbolAtLocation(element.name);
518+
if (!symbol) {
519+
return;
520+
}
521+
522+
// Visit recursive function navigate constructor
523+
const visit = (node: ts.Node) => {
524+
// Check if current node is identifier (variable)
525+
if (ts.isIdentifier(node)) {
526+
// Using the type checker, verify that this identifier refers
527+
// exactly to our destructured parameter and is not the node of the original declaration.
528+
if (localTypeChecker.getSymbolAtLocation(node) === symbol && node !== element.name) {
529+
// If the identifier is used as a shorthand property in an object literal (e.g., { myVar }),
530+
// must replace the entire `ShorthandPropertyAssignment` node
531+
// with a `PropertyAssignment` (e.g., myVar: this.myVar).
532+
if (ts.isShorthandPropertyAssignment(node.parent)) {
533+
tracker.replaceNode(
534+
node.parent,
535+
ts.factory.createPropertyAssignment(
536+
node,
537+
ts.factory.createPropertyAccessExpression(ts.factory.createThis(), propertyName),
538+
),
539+
);
540+
} else {
541+
// Otherwise, replace the identifier with `this.propertyName`.
542+
tracker.replaceNode(
543+
node,
544+
ts.factory.createPropertyAccessExpression(ts.factory.createThis(), propertyName),
545+
);
546+
}
547+
}
548+
}
549+
ts.forEachChild(node, visit);
550+
};
551+
552+
visit(constructorDecl.body);
553+
}
554+
555+
function createPropertyForBindingElement(
556+
context: ParameterMigrationContext,
557+
propertyName: string,
558+
propertyAccess: string,
559+
): void {
560+
const {node, memberIndentation, propsToAdd} = context;
561+
562+
const prop = ts.factory.createPropertyDeclaration(
563+
cloneModifiers(
564+
node.modifiers?.filter((modifier) => {
565+
return !ts.isDecorator(modifier) && modifier.kind !== ts.SyntaxKind.PublicKeyword;
566+
}),
567+
),
568+
propertyName,
569+
node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword)
570+
? undefined
571+
: node.questionToken,
572+
undefined,
573+
ts.factory.createIdentifier(PLACEHOLDER),
574+
);
575+
576+
propsToAdd.push(
577+
memberIndentation +
578+
replaceNodePlaceholder(node.getSourceFile(), prop, propertyAccess, context.printer),
579+
);
580+
}
581+
394582
/**
395583
* Creates a replacement `inject` call from a function parameter.
396584
* @param param Parameter for which to generate the `inject` call.

packages/core/schematics/test/inject_migration_spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,37 @@ describe('inject migration', () => {
529529
]);
530530
});
531531

532+
it('should migrate destructuring property', async () => {
533+
writeFile(
534+
'/dir.ts',
535+
[
536+
`import { Directive, ElementRef } from '@angular/core';`,
537+
``,
538+
`@Directive()`,
539+
`class MyDir {`,
540+
` constructor({nativeElement}: ElementRef) {`,
541+
` console.log(nativeElement);`,
542+
` }`,
543+
`}`,
544+
].join('\n'),
545+
);
546+
547+
await runMigration();
548+
549+
expect(tree.readContent('/dir.ts').split('\n')).toEqual([
550+
`import { Directive, ElementRef, inject } from '@angular/core';`,
551+
``,
552+
`@Directive()`,
553+
`class MyDir {`,
554+
` nativeElement = inject(ElementRef).nativeElement;`,
555+
``,
556+
` constructor() {`,
557+
` console.log(this.nativeElement);`,
558+
` }`,
559+
`}`,
560+
]);
561+
});
562+
532563
it('should preserve the constructor if it has other expressions', async () => {
533564
writeFile(
534565
'/dir.ts',

0 commit comments

Comments
 (0)