@@ -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.
0 commit comments