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