Skip to content

Commit f9d33e6

Browse files
slapec93Gergely Békési
andauthored
fix: codemod gaps (#1246)
* fix: improve codemod logic to cover more edge cases * fix: handle spyOn and import statements --------- Co-authored-by: Gergely Békési <gergely.bekesi@ethswarm.org>
1 parent c78f60a commit f9d33e6

1 file changed

Lines changed: 311 additions & 0 deletions

File tree

codemod/transforms/v13.ts

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,259 @@ export function transform(sourceFile: ts.SourceFile, checker: ts.TypeChecker): s
242242
})
243243
}
244244

245+
// ChunkBuilder.hash() now returns a Reference, not a raw Uint8Array.
246+
const isChunkBuilderReceiver = (expr: ts.Expression): boolean => {
247+
const symbol = checker.getTypeAtLocation(expr).getSymbol()
248+
249+
if (!symbol || symbol.getName() !== 'ChunkBuilder') {
250+
return false
251+
}
252+
253+
return (symbol.getDeclarations() ?? []).some(decl => {
254+
if (!ts.isClassDeclaration(decl)) {
255+
return false
256+
}
257+
258+
const file = decl.getSourceFile().fileName
259+
260+
return /[/\\]core-sdk[/\\]/.test(file) || /[/\\]splitter\.(d\.)?ts$/.test(file)
261+
})
262+
}
263+
245264
const replacements: Array<{ start: number; end: number; text: string }> = []
246265

266+
// MerkleTree was removed in v13; ChunkSplitter is the replacement.
267+
let merkleTreeLocalName: string | null = null
268+
let merkleTreeImportNameNode: ts.Identifier | null = null
269+
270+
for (const stmt of sourceFile.statements) {
271+
if (
272+
ts.isImportDeclaration(stmt) &&
273+
ts.isStringLiteral(stmt.moduleSpecifier) &&
274+
stmt.moduleSpecifier.text === '@ethersphere/bee-js' &&
275+
stmt.importClause?.namedBindings &&
276+
ts.isNamedImports(stmt.importClause.namedBindings)
277+
) {
278+
for (const element of stmt.importClause.namedBindings.elements) {
279+
const importedNameNode = element.propertyName ?? element.name
280+
281+
if (importedNameNode.text === 'MerkleTree') {
282+
// Aliased imports keep their local name unchanged.
283+
replacements.push({
284+
start: importedNameNode.getStart(sourceFile),
285+
end: importedNameNode.getEnd(),
286+
text: 'ChunkSplitter',
287+
})
288+
289+
if (!element.propertyName) {
290+
merkleTreeLocalName = element.name.text
291+
merkleTreeImportNameNode = element.name
292+
}
293+
}
294+
}
295+
}
296+
297+
// @upcoming/swarm-core is now @ethersphere/core-sdk.
298+
if (
299+
(ts.isImportDeclaration(stmt) || ts.isExportDeclaration(stmt)) &&
300+
stmt.moduleSpecifier &&
301+
ts.isStringLiteral(stmt.moduleSpecifier) &&
302+
stmt.moduleSpecifier.text === '@upcoming/swarm-core'
303+
) {
304+
const quote = stmt.moduleSpecifier.getText(sourceFile)[0]!
305+
replacements.push({
306+
start: stmt.moduleSpecifier.getStart(sourceFile),
307+
end: stmt.moduleSpecifier.getEnd(),
308+
text: `${quote}@ethersphere/core-sdk${quote}`,
309+
})
310+
}
311+
}
312+
313+
// Syntactic fallback: MerkleTree no longer type-checks, so isChunkBuilderReceiver alone
314+
// won't find it.
315+
const unwrap = (expr: ts.Expression): ts.Expression => {
316+
while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) {
317+
expr = expr.expression
318+
}
319+
320+
return expr
321+
}
322+
323+
const isChunkSplitterRootCall = (expr: ts.Expression): boolean => {
324+
const inner = unwrap(expr)
325+
326+
return (
327+
merkleTreeLocalName !== null &&
328+
ts.isCallExpression(inner) &&
329+
ts.isPropertyAccessExpression(inner.expression) &&
330+
ts.isIdentifier(inner.expression.expression) &&
331+
inner.expression.expression.text === merkleTreeLocalName &&
332+
(inner.expression.name.text === 'root' || inner.expression.name.text === 'finalize')
333+
)
334+
}
335+
336+
const chunkBuilderVarNames = new Set<string>()
337+
338+
const collectChunkBuilderVars = (node: ts.Node): void => {
339+
if (
340+
ts.isVariableDeclaration(node) &&
341+
ts.isIdentifier(node.name) &&
342+
node.initializer &&
343+
isChunkSplitterRootCall(node.initializer)
344+
) {
345+
chunkBuilderVarNames.add(node.name.text)
346+
}
347+
348+
ts.forEachChild(node, collectChunkBuilderVars)
349+
}
350+
351+
if (merkleTreeLocalName) {
352+
collectChunkBuilderVars(sourceFile)
353+
}
354+
355+
// bee-js's MantarayNode.collect()/.find()/.findClosest() return core-sdk's MantarayNode,
356+
// not its own.
357+
const isMantarayNodeType = (type: ts.Type, fileTest: RegExp): boolean => {
358+
const symbol = type.getSymbol()
359+
360+
if (!symbol || symbol.getName() !== 'MantarayNode') {
361+
return false
362+
}
363+
364+
return (symbol.getDeclarations() ?? []).some(
365+
decl => ts.isClassDeclaration(decl) && fileTest.test(decl.getSourceFile().fileName),
366+
)
367+
}
368+
const isBeeMantarayNodeType = (type: ts.Type): boolean => isMantarayNodeType(type, /[/\\]bee-js[/\\]/)
369+
const isCoreMantarayNodeType = (type: ts.Type): boolean => isMantarayNodeType(type, /[/\\]core-sdk[/\\]/)
370+
371+
// Tracks for-of loop vars whose element type is core-sdk's MantarayNode.
372+
const coreFlavoredNames = new Set<string>()
373+
374+
const collectCoreFlavoredBindings = (node: ts.Node): void => {
375+
if (ts.isForOfStatement(node) && ts.isVariableDeclarationList(node.initializer)) {
376+
const decl = node.initializer.declarations[0]
377+
378+
if (decl && ts.isIdentifier(decl.name) && isCoreMantarayNodeType(checker.getTypeAtLocation(decl.name))) {
379+
coreFlavoredNames.add(decl.name.text)
380+
}
381+
}
382+
383+
ts.forEachChild(node, collectCoreFlavoredBindings)
384+
}
385+
collectCoreFlavoredBindings(sourceFile)
386+
387+
let needsCoreMantarayNodeImport = false
388+
389+
// Rewrites a parameter's type when called with a core-flavored argument.
390+
const rewriteMantarayNodeParameters = (node: ts.Node): void => {
391+
if (ts.isCallExpression(node)) {
392+
node.arguments.forEach((arg, index) => {
393+
if (!ts.isIdentifier(arg) || !coreFlavoredNames.has(arg.text)) {
394+
return
395+
}
396+
397+
const signature = checker.getResolvedSignature(node)
398+
const decl = signature?.getDeclaration()
399+
const param = decl && 'parameters' in decl ? decl.parameters[index] : undefined
400+
401+
if (
402+
param?.type &&
403+
ts.isTypeReferenceNode(param.type) &&
404+
isBeeMantarayNodeType(checker.getTypeFromTypeNode(param.type))
405+
) {
406+
replacements.push({
407+
start: param.type.getStart(sourceFile),
408+
end: param.type.getEnd(),
409+
text: 'CoreMantarayNode',
410+
})
411+
needsCoreMantarayNodeImport = true
412+
}
413+
})
414+
}
415+
416+
ts.forEachChild(node, rewriteMantarayNodeParameters)
417+
}
418+
419+
// Rewrites a Map/Array/Set's value type when a core-flavored value is inserted.
420+
const rewriteMantarayNodeContainers = (node: ts.Node): void => {
421+
if (
422+
ts.isCallExpression(node) &&
423+
ts.isPropertyAccessExpression(node.expression) &&
424+
['set', 'push', 'add'].includes(node.expression.name.text)
425+
) {
426+
const valueArg = node.expression.name.text === 'set' ? node.arguments[1] : node.arguments[0]
427+
428+
if (valueArg && ts.isIdentifier(valueArg) && coreFlavoredNames.has(valueArg.text)) {
429+
const receiverSymbol = checker.getSymbolAtLocation(node.expression.expression)
430+
const receiverDecl = receiverSymbol?.declarations?.find(ts.isVariableDeclaration)
431+
432+
// Type args may be on the declaration or the constructor call.
433+
const typeArguments =
434+
receiverDecl?.type && ts.isTypeReferenceNode(receiverDecl.type)
435+
? receiverDecl.type.typeArguments
436+
: receiverDecl?.initializer && ts.isNewExpression(receiverDecl.initializer)
437+
? receiverDecl.initializer.typeArguments
438+
: undefined
439+
440+
if (typeArguments) {
441+
for (const typeArg of typeArguments) {
442+
if (ts.isTypeReferenceNode(typeArg) && isBeeMantarayNodeType(checker.getTypeFromTypeNode(typeArg))) {
443+
replacements.push({
444+
start: typeArg.getStart(sourceFile),
445+
end: typeArg.getEnd(),
446+
text: 'CoreMantarayNode',
447+
})
448+
needsCoreMantarayNodeImport = true
449+
}
450+
}
451+
}
452+
}
453+
}
454+
455+
ts.forEachChild(node, rewriteMantarayNodeContainers)
456+
}
457+
458+
if (coreFlavoredNames.size > 0) {
459+
rewriteMantarayNodeParameters(sourceFile)
460+
rewriteMantarayNodeContainers(sourceFile)
461+
}
462+
463+
if (needsCoreMantarayNodeImport) {
464+
const coreSdkImport = sourceFile.statements.find(
465+
(stmt): stmt is ts.ImportDeclaration =>
466+
ts.isImportDeclaration(stmt) &&
467+
ts.isStringLiteral(stmt.moduleSpecifier) &&
468+
stmt.moduleSpecifier.text === '@ethersphere/core-sdk',
469+
)
470+
471+
if (coreSdkImport?.importClause?.namedBindings && ts.isNamedImports(coreSdkImport.importClause.namedBindings)) {
472+
const elements = coreSdkImport.importClause.namedBindings.elements
473+
const lastSpecifier = elements[elements.length - 1]
474+
475+
if (lastSpecifier) {
476+
replacements.push({
477+
start: lastSpecifier.getEnd(),
478+
end: lastSpecifier.getEnd(),
479+
text: ', MantarayNode as CoreMantarayNode',
480+
})
481+
}
482+
} else {
483+
const beeJsImport = sourceFile.statements.find(
484+
(stmt): stmt is ts.ImportDeclaration =>
485+
ts.isImportDeclaration(stmt) &&
486+
ts.isStringLiteral(stmt.moduleSpecifier) &&
487+
stmt.moduleSpecifier.text === '@ethersphere/bee-js',
488+
)
489+
const insertAt = beeJsImport ? beeJsImport.getEnd() : 0
490+
replacements.push({
491+
start: insertAt,
492+
end: insertAt,
493+
text: `\nimport { MantarayNode as CoreMantarayNode } from '@ethersphere/core-sdk'`,
494+
})
495+
}
496+
}
497+
247498
const visit = (node: ts.Node): void => {
248499
// Any `<bee>.<method>` access — called or not — so bare method references migrate too.
249500
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
@@ -261,6 +512,66 @@ export function transform(sourceFile: ts.SourceFile, checker: ts.TypeChecker): s
261512
}
262513
}
263514

515+
// jest.spyOn uses a string literal, so the rewrite above doesn't catch it.
516+
if (
517+
ts.isCallExpression(node) &&
518+
ts.isPropertyAccessExpression(node.expression) &&
519+
node.expression.name.text === 'spyOn' &&
520+
node.arguments.length >= 2
521+
) {
522+
const [receiverArg, methodArg] = node.arguments
523+
524+
if (receiverArg && methodArg && ts.isStringLiteralLike(methodArg)) {
525+
const mapping = METHOD_MAP[methodArg.text]
526+
527+
if (mapping && isBeeReceiver(receiverArg)) {
528+
const quote = methodArg.getText(sourceFile)[0]!
529+
replacements.push({
530+
start: receiverArg.getStart(sourceFile),
531+
end: receiverArg.getEnd(),
532+
text: `${receiverArg.getText(sourceFile)}.${mapping.namespace}`,
533+
})
534+
replacements.push({
535+
start: methodArg.getStart(sourceFile),
536+
end: methodArg.getEnd(),
537+
text: `${quote}${mapping.newName}${quote}`,
538+
})
539+
}
540+
}
541+
}
542+
543+
// ChunkBuilder.hash() now returns a Reference; append the conversion back.
544+
if (
545+
merkleTreeLocalName &&
546+
ts.isCallExpression(node) &&
547+
ts.isPropertyAccessExpression(node.expression) &&
548+
node.expression.name.text === 'hash' &&
549+
node.arguments.length === 0 &&
550+
(isChunkSplitterRootCall(node.expression.expression) ||
551+
(ts.isIdentifier(node.expression.expression) && chunkBuilderVarNames.has(node.expression.expression.text)) ||
552+
isChunkBuilderReceiver(node.expression.expression))
553+
) {
554+
replacements.push({
555+
start: node.getEnd(),
556+
end: node.getEnd(),
557+
text: '.toUint8Array()',
558+
})
559+
}
560+
561+
// Renames remaining MerkleTree usages (import itself already handled above).
562+
if (
563+
merkleTreeLocalName &&
564+
ts.isIdentifier(node) &&
565+
node.text === merkleTreeLocalName &&
566+
node !== merkleTreeImportNameNode
567+
) {
568+
replacements.push({
569+
start: node.getStart(sourceFile),
570+
end: node.getEnd(),
571+
text: 'ChunkSplitter',
572+
})
573+
}
574+
264575
ts.forEachChild(node, visit)
265576
}
266577
visit(sourceFile)

0 commit comments

Comments
 (0)