Skip to content

Commit ff3fcf4

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

2 files changed

Lines changed: 269 additions & 53 deletions

File tree

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

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

283+
interface ParameterMigrationContext {
284+
node: ts.ParameterDeclaration;
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,37 @@ 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);
351+
} else if (ts.isObjectBindingPattern(node.name)) {
352+
migrateObjectBindingParameter(context);
353+
} else {
316354
return;
317355
}
356+
}
318357

319-
const name = node.name.text;
358+
function migrateIdentifierParameter(context: ParameterMigrationContext): void {
359+
const {node, options, localTypeChecker, printer, tracker, usedInConstructor, usesOtherParams} =
360+
context;
361+
362+
const name = (node.name as ts.Identifier).text;
320363
const replacementCall = createInjectReplacementCall(
321364
node,
322365
options,
@@ -328,69 +371,211 @@ function migrateParameter(
328371

329372
// If the parameter declares a property, we need to declare it (e.g. `private foo: Foo`).
330373
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-
);
374+
handlePropertyDeclaration(context, name, replacementCall);
354375
}
355376

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

386435
if (superCall === null) {
387-
prependToConstructor.push(toAdd);
436+
prependToConstructor.push(initializer);
388437
} else {
389-
afterSuper.push(toAdd);
438+
afterSuper.push(initializer);
390439
}
440+
} else {
441+
// If the parameter is only referenced in the constructor, we
442+
// don't need to declare any new properties.
443+
prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
444+
}
445+
}
446+
447+
function handleParameterWithDependencies(
448+
context: ParameterMigrationContext,
449+
name: string,
450+
replacementCall: string,
451+
): void {
452+
const {innerIndentation, prependToConstructor, afterSuper, superCall} = context;
453+
454+
const toAdd = `${innerIndentation}this.${name} = ${replacementCall};`;
455+
456+
if (superCall === null) {
457+
prependToConstructor.push(toAdd);
458+
} else {
459+
afterSuper.push(toAdd);
391460
}
392461
}
393462

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