1- import type { NodePath } from "@babel/traverse"
1+ import type { NodePath , Scope } from "@babel/traverse"
22import traverse from "@babel/traverse"
3- import type { BlockStatement , CallExpression , File , FunctionDeclaration , Identifier } from "@babel/types"
3+ import type { ArrayExpression , Block , BlockStatement , CallExpression , File , FunctionDeclaration , Identifier , Node , ObjectExpression } from "@babel/types"
44import t from "@babel/types"
55import type { LaxPartial } from "@samual/lib"
66import { assert } from "@samual/lib/assert"
@@ -675,6 +675,106 @@ export function transform(
675675 ) )
676676 }
677677
678+ const getFirstParentBlock = ( path : NodePath ) : Block => {
679+ let someBlock : Block | null = null
680+ let currentParent : NodePath < any > | null = path
681+ while ( currentParent ) {
682+ if ( ! currentParent || ! currentParent . node ) break
683+
684+ if ( t . isBlock ( currentParent . node ) ) {
685+ someBlock = currentParent . node
686+ break
687+ } else if ( t . isArrowFunctionExpression ( currentParent . parentPath ?. node ) ) {
688+ // This means we're in an arrow function like () => 1.
689+ // The arrow function can have a block, as a treat
690+ currentParent . replaceWith (
691+ t . blockStatement ( [
692+ t . returnStatement (
693+ currentParent . node ,
694+ ) ,
695+ ] ) ,
696+ )
697+ someBlock = currentParent . node
698+ break
699+ }
700+
701+ currentParent = currentParent . parentPath
702+ }
703+
704+ // Technically, this can't happen, since the Program node will be a block.
705+ assert ( someBlock != null , HERE )
706+ return someBlock ;
707+ }
708+
709+ const replaceAllThisWith = ( node : Node , scope : Scope , thisId : string ) : boolean => {
710+ let thisIsReferenced = false
711+ traverse ( node , {
712+ ThisExpression ( path ) {
713+ thisIsReferenced = true
714+ path . replaceWith ( t . identifier ( thisId ) )
715+ } ,
716+ Function ( path ) {
717+ if ( path . node . type != `ArrowFunctionExpression` ) {
718+ path . skip ( )
719+ }
720+ }
721+ } , scope )
722+
723+ return thisIsReferenced
724+ }
725+
726+ type ObjectLikeExpression = ObjectExpression | ArrayExpression
727+ const replaceThisInObjectLikeDefinition = < T extends ObjectLikeExpression > ( path : NodePath < T > ) => {
728+ const { node : object , scope, parent } = path
729+
730+ const evenMoreUniqueId = Math . floor ( Math . random ( ) * ( 2 ** 52 ) ) . toString ( 36 ) . padStart ( 11 , `0` )
731+
732+ // This removes the additional let that would normally be inserted from this sort of construct:
733+ // const foo = {
734+ // bar() { this.whatever = 1 }
735+ // }
736+ const reuseDeclaredName = parent . type == `VariableDeclarator`
737+ && path . parentPath ?. parentPath ?. node ?. type == `VariableDeclaration`
738+ && path . parentPath ?. parentPath ?. node ?. kind == `const` // This is only safe if it's not redeclared!
739+ && parent . id . type == `Identifier`
740+
741+ let thisId = reuseDeclaredName ? ( parent . id as Identifier ) . name : `_${ evenMoreUniqueId } _THIS_`
742+
743+ let thisIsReferenced = false
744+ if ( path . type == `ObjectExpression` ) {
745+ for ( const property of ( object as ObjectExpression ) . properties ) {
746+ if ( property . type != `ObjectMethod` )
747+ continue
748+
749+ thisIsReferenced ||= replaceAllThisWith ( object , scope , thisId )
750+ }
751+ } else {
752+ for ( const element of ( object as ArrayExpression ) . elements ) {
753+ if ( element == null )
754+ continue
755+
756+ thisIsReferenced ||= replaceAllThisWith ( element , scope , thisId )
757+ }
758+ }
759+
760+ if ( ! thisIsReferenced ) return
761+ if ( reuseDeclaredName ) return
762+
763+ path . replaceWith (
764+ t . assignmentExpression ( `=` , t . identifier ( thisId ) , object )
765+ )
766+
767+ const parentBlock = getFirstParentBlock ( path ) ;
768+ parentBlock . body . unshift (
769+ t . variableDeclaration ( `let` , [
770+ t . variableDeclarator (
771+ t . identifier ( thisId ) ,
772+ null
773+ ) ,
774+ ] ) ,
775+ )
776+ }
777+
678778 traverse ( file , {
679779 BlockStatement ( { node : blockStatement } ) {
680780 for ( const [ index , functionDeclaration ] of blockStatement . body . entries ( ) ) {
@@ -695,81 +795,10 @@ export function transform(
695795 }
696796 } ,
697797 ObjectExpression ( path ) {
698- const { node : object , scope, parent } = path
699-
700- const evenMoreUniqueId = Math . floor ( Math . random ( ) * ( 2 ** 52 ) ) . toString ( 36 ) . padStart ( 11 , `0` )
701-
702- // This removes the additional let that would normally be inserted from this sort of construct:
703- // const foo = {
704- // bar() { this.whatever = 1 }
705- // }
706- const reuseDeclaredName = parent . type == `VariableDeclarator`
707- && path . parentPath ?. parentPath ?. node ?. type == `VariableDeclaration`
708- && path . parentPath ?. parentPath ?. node ?. kind == `const` // This is only safe if it's not redeclared!
709- && parent . id . type == `Identifier`
710-
711- let thisId = reuseDeclaredName ? ( parent . id as Identifier ) . name : `_${ evenMoreUniqueId } _THIS_`
712-
713- let thisIsReferenced = false
714- for ( const property of object . properties ) {
715- if ( property . type != `ObjectMethod` )
716- continue
717-
718- traverse ( property . body , {
719- ThisExpression ( path ) {
720- thisIsReferenced = true
721- path . replaceWith ( t . identifier ( thisId ) )
722- } ,
723- Function ( path ) {
724- if ( path . node . type != `ArrowFunctionExpression` ) {
725- path . skip ( )
726- }
727- }
728- } , scope ) ;
729- }
730-
731- if ( ! thisIsReferenced ) return
732- if ( reuseDeclaredName ) return
733-
734- path . replaceWith (
735- t . assignmentExpression ( `=` , t . identifier ( thisId ) , object )
736- )
737-
738- let someBlock = null
739- let currentParent : NodePath < any > | null = path
740- while ( currentParent ) {
741- if ( ! currentParent || ! currentParent . node ) break
742-
743- if ( t . isBlock ( currentParent . node ) ) {
744- someBlock = currentParent . node
745- break
746- } else if ( t . isArrowFunctionExpression ( currentParent . parentPath ?. node ) ) {
747- // This means we're in an arrow function like () => 1.
748- // The arrow function can have a block, as a treat
749- currentParent . replaceWith (
750- t . blockStatement ( [
751- t . returnStatement (
752- currentParent . node ,
753- ) ,
754- ] ) ,
755- )
756- someBlock = currentParent . node
757- break
758- }
759-
760- currentParent = currentParent . parentPath
761- }
762-
763- assert ( someBlock != null , HERE )
764-
765- someBlock ! . body . unshift (
766- t . variableDeclaration ( `let` , [
767- t . variableDeclarator (
768- t . identifier ( thisId ) ,
769- null
770- ) ,
771- ] ) ,
772- )
798+ replaceThisInObjectLikeDefinition ( path )
799+ } ,
800+ ArrayExpression ( path ) {
801+ replaceThisInObjectLikeDefinition ( path )
773802 } ,
774803 ClassBody ( { node : classBody , scope, parent } ) {
775804 assert ( t . isClass ( parent ) , HERE )
0 commit comments