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