1
1
import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement" ;
2
2
import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call" ;
3
+ import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path" ;
3
4
import type { SgRoot , Edit , SgNode } from "@codemod.com/jssg-types/main" ;
4
5
import type Js from "@codemod.com/jssg-types/langs/javascript" ;
5
6
@@ -19,95 +20,98 @@ export default function transform(root: SgRoot<Js>): string | null {
19
20
const rootNode = root . root ( ) ;
20
21
const edits : Edit [ ] = [ ] ;
21
22
22
- // Track what imports need to be updated
23
+ // Bindings we care about and their replacements for truncate -> ftruncate
24
+ const checks = [
25
+ {
26
+ path : "$.truncate" ,
27
+ prop : "truncate" ,
28
+ replaceFn : ( name : string ) => name . replace ( / t r u n c a t e $ / , "ftruncate" ) ,
29
+ isSync : false
30
+ } ,
31
+ {
32
+ path : "$.truncateSync" ,
33
+ prop : "truncateSync" ,
34
+ replaceFn : ( name : string ) => name . replace ( / t r u n c a t e S y n c $ / , "ftruncateSync" ) ,
35
+ isSync : true
36
+ } ,
37
+ {
38
+ path : "$.promises.truncate" ,
39
+ prop : "truncate" ,
40
+ replaceFn : ( name : string ) => name . replace ( / t r u n c a t e $ / , "ftruncate" ) ,
41
+ isSync : false
42
+ } ,
43
+ ] ;
44
+
45
+ // Gather fs import/require statements to resolve local binding names
46
+ const stmtNodes = [
47
+ ...getNodeRequireCalls ( root , "fs" ) ,
48
+ ...getNodeImportStatements ( root , "fs" ) ,
49
+ ] ;
50
+
23
51
let usedTruncate = false ;
24
52
let usedTruncateSync = false ;
25
53
26
- // Find fs.truncate and fs.truncateSync calls (these are always safe to transform)
27
- const fsTruncateCalls = rootNode . findAll ( {
28
- rule : {
29
- any : [
30
- { pattern : "fs.truncate($FD, $LEN, $CALLBACK)" } ,
31
- { pattern : "fs.truncate($FD, $LEN)" } ,
32
- { pattern : "fs.truncateSync($FD, $LEN)" }
33
- ]
34
- }
35
- } ) ;
54
+ for ( const stmt of stmtNodes ) {
55
+ for ( const check of checks ) {
56
+ const local = resolveBindingPath ( stmt , check . path ) ;
57
+ if ( ! local ) continue ;
36
58
37
- // Transform fs.truncate calls
38
- for ( const call of fsTruncateCalls ) {
39
- const fdMatch = call . getMatch ( "FD" ) ;
40
- const lenMatch = call . getMatch ( "LEN" ) ;
41
- const callbackMatch = call . getMatch ( "CALLBACK" ) ;
42
-
43
- if ( ! fdMatch || ! lenMatch ) continue ;
44
-
45
- const fd = fdMatch . text ( ) ;
46
- const len = lenMatch . text ( ) ;
47
- const callback = callbackMatch ?. text ( ) ;
48
- const callText = call . text ( ) ;
49
-
50
- let newCallText : string ;
51
- if ( callText . includes ( "fs.truncateSync(" ) ) {
52
- newCallText = `fs.ftruncateSync(${ fd } , ${ len } )` ;
53
- } else {
54
- newCallText = callback
55
- ? `fs.ftruncate(${ fd } , ${ len } , ${ callback } )`
56
- : `fs.ftruncate(${ fd } , ${ len } )` ;
57
- }
59
+ // property name to look for on fs (e.g. 'truncate' or 'truncateSync')
60
+ const propName = check . prop ;
58
61
59
- edits . push ( call . replace ( newCallText ) ) ;
60
- }
62
+ // Find call sites for the resolved local binding and for fs.<prop>
63
+ const calls = rootNode . findAll ( {
64
+ rule : {
65
+ any : [
66
+ { pattern : `${ local } ($FD, $LEN, $CALLBACK)` } ,
67
+ { pattern : `${ local } ($FD, $LEN)` } ,
68
+ { pattern : `fs.${ propName } ($FD, $LEN, $CALLBACK)` } ,
69
+ { pattern : `fs.${ propName } ($FD, $LEN)` } ,
70
+ ] ,
71
+ } ,
72
+ } ) ;
61
73
62
- // Find destructured truncate/truncateSync calls (need scope analysis)
63
- const destructuredCalls = rootNode . findAll ( {
64
- rule : {
65
- any : [
66
- { pattern : "truncate($FD, $LEN, $CALLBACK)" } ,
67
- { pattern : "truncate($FD, $LEN)" } ,
68
- { pattern : "truncateSync($FD, $LEN)" }
69
- ]
70
- }
71
- } ) ;
74
+ let transformedAny = false ;
75
+ for ( const call of calls ) {
76
+ const fdMatch = call . getMatch ( "FD" ) ;
77
+ if ( ! fdMatch ) continue ;
78
+ const fdText = fdMatch . text ( ) ;
72
79
73
- // Transform destructured calls only if they're from fs imports/requires
74
- for ( const call of destructuredCalls ) {
75
- if ( isFromFsModule ( call , root ) ) {
76
- const fdMatch = call . getMatch ( "FD" ) ;
77
- const lenMatch = call . getMatch ( "LEN" ) ;
78
- const callbackMatch = call . getMatch ( "CALLBACK" ) ;
79
-
80
- if ( ! fdMatch || ! lenMatch ) continue ;
81
-
82
- const fd = fdMatch . text ( ) ;
83
- const len = lenMatch . text ( ) ;
84
- const callback = callbackMatch ?. text ( ) ;
85
- const callText = call . text ( ) ;
86
-
87
- // Check if this looks like a file descriptor
88
- if ( isLikelyFileDescriptor ( fd , rootNode ) ) {
89
- let newCallText : string ;
90
-
91
- if ( callText . includes ( "truncateSync(" ) ) {
92
- newCallText = `ftruncateSync(${ fd } , ${ len } )` ;
93
- usedTruncateSync = true ;
94
- } else {
95
- newCallText = callback
96
- ? `ftruncate(${ fd } , ${ len } , ${ callback } )`
97
- : `ftruncate(${ fd } , ${ len } )` ;
98
- usedTruncate = true ;
99
- }
80
+ // only transform when first arg is likely a file descriptor
81
+ if ( ! isLikelyFileDescriptor ( fdText , rootNode ) ) continue ;
82
+
83
+ // Replace callee name (handles local alias and fs.<prop> usages)
84
+ let newCallText = call . text ( ) ;
85
+
86
+ // Replace occurrences of the local binding (e.g. "truncate" or alias)
87
+
88
+ const escapedLocal = local . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
89
+ newCallText = newCallText . replace ( new RegExp ( `\\b${ escapedLocal } \\b` ) , check . replaceFn ( local ) ) ;
90
+
91
+ // Also replace fs.truncate / fs.truncateSync usages
92
+ newCallText = newCallText . replace ( new RegExp ( `\\bfs\\.${ propName } \\b` , "g" ) , `fs.${ check . replaceFn ( propName ) } ` ) ;
100
93
101
94
edits . push ( call . replace ( newCallText ) ) ;
95
+ transformedAny = true ;
96
+ if ( check . isSync ) usedTruncateSync = true ; else usedTruncate = true ;
97
+ }
98
+
99
+ // Update import/destructure to include/rename to ftruncate/ftruncateSync where necessary
100
+ const namedNode = stmt . find ( { rule : { kind : "object_pattern" } } ) || stmt . find ( { rule : { kind : "named_imports" } } ) ;
101
+ if ( transformedAny && namedNode ?. text ( ) . includes ( propName ) ) {
102
+ const original = namedNode . text ( ) ;
103
+ const newText = original . replace ( new RegExp ( `\\b${ propName } \\b` , "g" ) , check . replaceFn ( propName ) ) ;
104
+ if ( newText !== original ) {
105
+ edits . push ( namedNode . replace ( newText ) ) ;
106
+ }
102
107
}
103
108
}
104
109
}
105
110
106
- // Update imports/requires if we have destructured calls that were transformed
107
- if ( usedTruncate || usedTruncateSync ) {
108
- updateImportsAndRequires ( root , usedTruncate , usedTruncateSync , edits ) ;
109
- }
110
- if ( edits . length === 0 ) return null ;
111
+ // Update import/require statements to reflect renamed bindings
112
+ updateImportsAndRequires ( root , usedTruncate , usedTruncateSync , edits ) ;
113
+
114
+ if ( ! edits . length ) return null ;
111
115
112
116
return rootNode . commitEdits ( edits ) ;
113
117
}
@@ -116,9 +120,7 @@ export default function transform(root: SgRoot<Js>): string | null {
116
120
* Update import and require statements to replace truncate functions with ftruncate
117
121
*/
118
122
function updateImportsAndRequires ( root : SgRoot < Js > , usedTruncate : boolean , usedTruncateSync : boolean , edits : Edit [ ] ) : void {
119
- // @ts -ignore - ast-grep types are not fully compatible with JSSG types
120
123
const importStatements = getNodeImportStatements ( root , 'fs' ) ;
121
- // @ts -ignore - ast-grep types are not fully compatible with JSSG types
122
124
const requireStatements = getNodeRequireCalls ( root , 'fs' ) ;
123
125
124
126
// Update import and require statements
@@ -142,31 +144,6 @@ function updateImportsAndRequires(root: SgRoot<Js>, usedTruncate: boolean, usedT
142
144
}
143
145
}
144
146
145
- /**
146
- * Check if a call expression is from a destructured fs import/require
147
- */
148
- function isFromFsModule ( call : SgNode < Js > , root : SgRoot < Js > ) : boolean {
149
- // @ts -ignore - ast-grep types are not fully compatible with JSSG types
150
- const importStatements = getNodeImportStatements ( root , 'fs' ) ;
151
- // @ts -ignore - ast-grep types are not fully compatible with JSSG types
152
- const requireStatements = getNodeRequireCalls ( root , 'fs' ) ;
153
-
154
- // Get the function name being called (truncate or truncateSync)
155
- const callExpression = call . child ( 0 ) ;
156
- const functionName = callExpression ?. text ( ) ;
157
- if ( ! functionName ) return false ;
158
-
159
- // Check if this function name appears in any fs import/require destructuring
160
- for ( const statement of [ ...importStatements , ...requireStatements ] ) {
161
- const text = statement . text ( ) ;
162
- if ( text . includes ( "{" ) && text . includes ( functionName ) ) {
163
- return true ;
164
- }
165
- }
166
-
167
- return false ;
168
- }
169
-
170
147
/**
171
148
* Helper function to determine if a parameter is likely a file descriptor
172
149
* rather than a file path string.
0 commit comments