Skip to content

Commit 0dfe02b

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 0dfe02b

2 files changed

Lines changed: 271 additions & 52 deletions

File tree

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

Lines changed: 240 additions & 52 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,9 +329,38 @@ 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: node as ts.ParameterDeclaration & { name: ts.Identifier }
353+
});
354+
} else if (ts.isObjectBindingPattern(node.name)) {
355+
migrateObjectBindingParameter(context);
356+
} else {
316357
return;
317358
}
359+
}
360+
361+
function migrateIdentifierParameter(context: ParameterMigrationContext<ts.ParameterDeclaration & { name: ts.Identifier }>): void {
362+
const {node, options, localTypeChecker, printer, tracker, usedInConstructor, usesOtherParams} =
363+
context;
318364

319365
const name = node.name.text;
320366
const replacementCall = createInjectReplacementCall(
@@ -328,69 +374,211 @@ function migrateParameter(
328374

329375
// If the parameter declares a property, we need to declare it (e.g. `private foo: Foo`).
330376
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-
);
377+
handlePropertyDeclaration(context, name, replacementCall);
354378
}
355379

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

386438
if (superCall === null) {
387-
prependToConstructor.push(toAdd);
439+
prependToConstructor.push(initializer);
388440
} else {
389-
afterSuper.push(toAdd);
441+
afterSuper.push(initializer);
390442
}
443+
} else {
444+
// If the parameter is only referenced in the constructor, we
445+
// don't need to declare any new properties.
446+
prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
447+
}
448+
}
449+
450+
function handleParameterWithDependencies(
451+
context: ParameterMigrationContext,
452+
name: string,
453+
replacementCall: string,
454+
): void {
455+
const {innerIndentation, prependToConstructor, afterSuper, superCall} = context;
456+
457+
const toAdd = `${innerIndentation}this.${name} = ${replacementCall};`;
458+
459+
if (superCall === null) {
460+
prependToConstructor.push(toAdd);
461+
} else {
462+
afterSuper.push(toAdd);
391463
}
392464
}
393465

466+
function migrateObjectBindingParameter(context: ParameterMigrationContext): void {
467+
const {node, options, localTypeChecker, printer, tracker} = context;
468+
469+
const replacementCall = createInjectReplacementCall(
470+
node,
471+
options,
472+
localTypeChecker,
473+
printer,
474+
tracker,
475+
);
476+
477+
for (const element of (node.name as ts.ObjectBindingPattern).elements) {
478+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
479+
migrateBindingElement(context, element, replacementCall);
480+
}
481+
}
482+
}
483+
484+
function migrateBindingElement(
485+
context: ParameterMigrationContext,
486+
element: ts.BindingElement,
487+
replacementCall: string,
488+
): void {
489+
const propertyName = (element.name as ts.Identifier).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 as ts.TypeChecker).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
@@ -479,6 +479,37 @@ describe('inject migration', () => {
479479
]);
480480
});
481481

482+
it('should migrate destructoring 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)