|
| 1 | +//Simple parser for .sol files that finds libraries defined in the file and creates a wrapper |
| 2 | +// that converts all their internal functions to external functions so they can be tested |
| 3 | +// in forge unit tests. |
| 4 | +//Instead of properly parsing the full AST, we make our task of finding and transforming the |
| 5 | +// relevant bits easier by making some basic assumptions about code formatting that any sane |
| 6 | +// code should almost certainly adhere to regardless. |
| 7 | + |
| 8 | +//Solidity language grammar: |
| 9 | +// https://docs.soliditylang.org/en/latest/grammar.html |
| 10 | + |
| 11 | +import { readFileSync } from "fs"; |
| 12 | + |
| 13 | +// const commentTestCode = ` |
| 14 | +// // /* commented-out block comment start |
| 15 | +// code |
| 16 | +// // commented-out block comment end */ |
| 17 | +// const x = 42; // inline comment |
| 18 | +// /* block comment // */ |
| 19 | +// code after comment |
| 20 | +// // commented-out block comment end */ |
| 21 | +// `; |
| 22 | +function removeComments(code: string): string { |
| 23 | + //?<! is a negative lookbehind assertion: |
| 24 | + // it only finds matches that are not preceded by the specified pattern) |
| 25 | + //so in our case, we are looking for /* that is not preceded by // to make sure |
| 26 | + // that we only delete legitimate block comments |
| 27 | + //\S matches any non-whitespace character and used together with \s in [\s\S] |
| 28 | + // it matches any character including newlines (when .* would not) |
| 29 | + const blockCommentRegex = /(?<!\/\/.*)\/\*[\s\S]*?\*\//g; |
| 30 | + //m flag is multiline mode: |
| 31 | + // ensures that ^ and $ matches individual line starts/ends rather than the whole string |
| 32 | + const lineCommentRegex = /\/\/.*$/gm; |
| 33 | + const emptyLineRegex = /(^\s*\n)+/gm; |
| 34 | + |
| 35 | + return code |
| 36 | + .replace(blockCommentRegex, '') |
| 37 | + .replace(lineCommentRegex, '') |
| 38 | + .replace(emptyLineRegex, '\n'); |
| 39 | +} |
| 40 | + |
| 41 | +if (process.argv.length != 3) { |
| 42 | + console.log("requires exactly one command line argument relative to src: <path/libfile[.sol]>"); |
| 43 | + process.exit(1); |
| 44 | +} |
| 45 | + |
| 46 | +const filename = process.argv[2]; |
| 47 | +let fileCode = readFileSync("../src/" + filename + filename.endsWith(".sol") ? "" : ".sol", "utf8"); |
| 48 | + |
| 49 | +//we first remove all comments so we can be sure that everything we're parsing is actual code |
| 50 | +fileCode = removeComments(fileCode); |
| 51 | + |
| 52 | +interface Func { |
| 53 | + name: string; |
| 54 | + stateMut: string; |
| 55 | + paras: string[]; |
| 56 | + rets?: string[]; |
| 57 | +} |
| 58 | + |
| 59 | +const libraries: Record<string, Func[]> = {}; |
| 60 | + |
| 61 | +const libRegex = /library\s+(\w+)\s*\{([\s\S]*?)\n\}/g; |
| 62 | +//use library regex to find all libraries in the file and split into name and code pairs |
| 63 | +const libMatches = fileCode.matchAll(libRegex); |
| 64 | +for (const libs of libMatches) { |
| 65 | + const [_, name, code] = libs; |
| 66 | + libraries[name] = []; |
| 67 | + const structRegex = /\s*struct\s+(\w+)/g; |
| 68 | + const structs = new Set<string>(); |
| 69 | + const structMatches = code.matchAll(structRegex); |
| 70 | + for (const struct of structMatches) |
| 71 | + structs.add[struct[1]]; |
| 72 | + |
| 73 | + const funcRegex = /\s*function\s+(\w+)\s*\(([\s\S]*?)\)([\s\S]*?)([\{;])/g; |
| 74 | + const funcMatches = code.matchAll(funcRegex); |
| 75 | + for (const funcs of funcMatches) { |
| 76 | + const [_, funcName, funcParasRaw, modsRaw, close] = funcs; |
| 77 | + if (close == ';') |
| 78 | + continue; //function pointer, not a function definition |
| 79 | + |
| 80 | + if (!modsRaw.includes("internal")) |
| 81 | + continue; //not an internal function |
| 82 | + |
| 83 | + const retParasRegex = /returns\s*\(([\s\S]*?)\)/; |
| 84 | + const retParasRaw = modsRaw.match(retParasRegex); |
| 85 | + |
| 86 | + const collapseSpaceRegex = /\s\s+/g; |
| 87 | + const paramsToArray = (paramList: string) => |
| 88 | + paramList.replace(collapseSpaceRegex, ' ').trim().split(',').map(param => { |
| 89 | + param = param.trim(); |
| 90 | + const paraType = param.match(/^(\w+)/)[1]; |
| 91 | + return structs.has(paraType) ? param.replace(paraType, `${name}.${paraType}`) : param; |
| 92 | + }); |
| 93 | + |
| 94 | + libraries[name].push({ |
| 95 | + name: funcName, |
| 96 | + stateMut: modsRaw.match(/\b(pure|view)\b/)?.[0] ?? '', |
| 97 | + paras: paramsToArray(funcParasRaw.replace(/\bmemory\b/g, ' calldata ')), |
| 98 | + rets: retParasRaw ? paramsToArray(retParasRaw[1]) : undefined, |
| 99 | + }); |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +console.log(`// SPDX-License-Identifier: Apache 2 |
| 104 | +pragma solidity ^0.8.24; |
| 105 | +
|
| 106 | +//This file was auto-generated by libraryTestWrapper.ts |
| 107 | +
|
| 108 | +import "wormhole-sdk/${filename}.sol";`); |
| 109 | + |
| 110 | +const pConcat = (paras: string[]) => |
| 111 | + (paras.length > 2 ? "\n " : "") + |
| 112 | + paras.join(paras.length > 2 ? ",\n " : ", ") + |
| 113 | + (paras.length > 2 ? "\n " : ""); |
| 114 | + |
| 115 | +const pNames = (paras: string[]) => |
| 116 | + paras.map(para => { |
| 117 | + const pieces = para.split(" "); |
| 118 | + return pieces[pieces.length-1]; |
| 119 | + }); |
| 120 | + |
| 121 | + |
| 122 | +for (const [libName, funcs] of Object.entries(libraries)) { |
| 123 | + console.log(`\ncontract ${libName}TestWrapper {`); |
| 124 | + const funcCode = []; |
| 125 | + for (const func of funcs) |
| 126 | + funcCode.push([ |
| 127 | + ` function ${func.name}(${pConcat(func.paras)}) external ${func.stateMut}` + |
| 128 | + ` ${func.rets ? `returns (${pConcat(func.rets)}) ` : ''} {`, |
| 129 | + ` ${func.rets ? 'return ' : ''}${libName}.${func.name}(${pNames(func.paras).join(', ')});`, |
| 130 | + ` }` |
| 131 | + ].join('\n')); |
| 132 | + console.log(funcCode.join('\n\n')); |
| 133 | + console.log('}'); |
| 134 | +} |
0 commit comments