Skip to content

Commit 7b38c50

Browse files
committed
fix(core): Fixed inject migration schematics for migrate destructured properties
Fixes angular#62626 - Properties used with the destructor are also managed during migration.
1 parent c66e9fe commit 7b38c50

2 files changed

Lines changed: 271 additions & 53 deletions

File tree

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

Lines changed: 240 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,39 @@ 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(
351+
context,
352+
node.name.text
353+
);
354+
} else if (ts.isObjectBindingPattern(node.name)) {
355+
migrateObjectBindingParameter(context);
356+
} else {
316357
return;
317358
}
359+
}
360+
361+
function migrateIdentifierParameter(context: ParameterMigrationContext, name: string): void {
362+
const {node, options, localTypeChecker, printer, tracker, usedInConstructor, usesOtherParams} =
363+
context;
318364

319-
const name = node.name.text;
320365
const replacementCall = createInjectReplacementCall(
321366
node,
322367
options,
@@ -328,69 +373,211 @@ function migrateParameter(
328373

329374
// If the parameter declares a property, we need to declare it (e.g. `private foo: Foo`).
330375
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-
);
376+
handlePropertyDeclaration(context, name, replacementCall);
354377
}
355378

356379
// If the parameter is referenced within the constructor, we need to declare it as a variable.
357380
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-
}
381+
handleConstructorUsage(context, name, replacementCall, declaresProp);
383382
} else if (usesOtherParams && declaresProp) {
384-
const toAdd = `${innerIndentation}this.${name} = ${replacementCall};`;
383+
handleParameterWithDependencies(context, name, replacementCall);
384+
}
385+
}
386+
387+
function handlePropertyDeclaration(
388+
context: ParameterMigrationContext,
389+
name: string,
390+
replacementCall: string,
391+
): void {
392+
const {node, memberIndentation, propsToAdd} = context;
393+
394+
const canInitialize = !context.usedInSuper && !context.usesOtherParams;
395+
const prop = ts.factory.createPropertyDeclaration(
396+
cloneModifiers(
397+
node.modifiers?.filter((modifier) => {
398+
return !ts.isDecorator(modifier) && modifier.kind !== ts.SyntaxKind.PublicKeyword;
399+
}),
400+
),
401+
name,
402+
node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword)
403+
? undefined
404+
: node.questionToken,
405+
canInitialize ? undefined : node.type,
406+
canInitialize ? ts.factory.createIdentifier(PLACEHOLDER) : undefined,
407+
);
408+
409+
propsToAdd.push(
410+
memberIndentation +
411+
replaceNodePlaceholder(node.getSourceFile(), prop, replacementCall, context.printer),
412+
);
413+
}
414+
415+
function handleConstructorUsage(
416+
context: ParameterMigrationContext,
417+
name: string,
418+
replacementCall: string,
419+
declaresProp: boolean,
420+
): void {
421+
const {innerIndentation, prependToConstructor, afterSuper, superCall} = context;
422+
423+
if (context.usedInSuper) {
424+
// Usages of `this` aren't allowed before `super` calls so we need to
425+
// create a variable which calls `inject()` directly instead...
426+
prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
427+
428+
if (declaresProp) {
429+
afterSuper.push(`${innerIndentation}this.${name} = ${name};`);
430+
}
431+
} else if (declaresProp) {
432+
// If the parameter declares a property (`private foo: foo`) and is used inside the class
433+
// at the same time, we need to ensure that it's initialized to the value from the variable
434+
// and that we only reference `this` after the `super` call.
435+
const initializer = `${innerIndentation}const ${name} = this.${name};`;
385436

386437
if (superCall === null) {
387-
prependToConstructor.push(toAdd);
438+
prependToConstructor.push(initializer);
388439
} else {
389-
afterSuper.push(toAdd);
440+
afterSuper.push(initializer);
441+
}
442+
} else {
443+
// If the parameter is only referenced in the constructor, we
444+
// don't need to declare any new properties.
445+
prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
446+
}
447+
}
448+
449+
function handleParameterWithDependencies(
450+
context: ParameterMigrationContext,
451+
name: string,
452+
replacementCall: string,
453+
): void {
454+
const {innerIndentation, prependToConstructor, afterSuper, superCall} = context;
455+
456+
const toAdd = `${innerIndentation}this.${name} = ${replacementCall};`;
457+
458+
if (superCall === null) {
459+
prependToConstructor.push(toAdd);
460+
} else {
461+
afterSuper.push(toAdd);
462+
}
463+
}
464+
465+
function migrateObjectBindingParameter(context: ParameterMigrationContext): 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 (node.name as ts.ObjectBindingPattern).elements) {
477+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
478+
migrateBindingElement(context, element, replacementCall);
390479
}
391480
}
392481
}
393482

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

482+
it('should migrate destructuring property', async () => {
483+
writeFile(
484+
'/dir.ts',
485+
[
486+
`import { Directive, ElementRef } from '@angular/core';`,
487+
``,
488+
`@Directive()`,
489+
`class MyDir {`,
490+
` constructor({nativeElement}: ElementRef) {`,
491+
` console.log(nativeElement);`,
492+
` }`,
493+
`}`,
494+
].join('\n'),
495+
);
496+
497+
await runMigration();
498+
499+
expect(tree.readContent('/dir.ts').split('\n')).toEqual([
500+
`import { Directive, ElementRef, inject } from '@angular/core';`,
501+
``,
502+
`@Directive()`,
503+
`class MyDir {`,
504+
` nativeElement = inject(ElementRef).nativeElement;`,
505+
``,
506+
` constructor() {`,
507+
` console.log(this.nativeElement);`,
508+
` }`,
509+
`}`,
510+
]);
511+
});
512+
482513
it('should preserve the constructor if it has other expressions', async () => {
483514
writeFile(
484515
'/dir.ts',

0 commit comments

Comments
 (0)