recon-generate 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -1
- package/dist/link.d.ts +1 -1
- package/dist/link.js +65 -13
- package/dist/processor.js +175 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -163,6 +163,7 @@ async function main() {
|
|
|
163
163
|
.option('--medusa-config <path>', 'Path to medusa json (defaults based on --name)')
|
|
164
164
|
.option('--name <suite>', 'Suite name to pick config defaults (echidna-<name>.yaml / medusa-<name>.json)')
|
|
165
165
|
.option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
|
|
166
|
+
.option('--verbose', 'Enable verbose output for debugging')
|
|
166
167
|
.action(async (opts, cmd) => {
|
|
167
168
|
var _a;
|
|
168
169
|
const workspaceRoot = process.cwd();
|
|
@@ -182,7 +183,7 @@ async function main() {
|
|
|
182
183
|
const medusaConfigPath = path.isAbsolute(medusaConfigOpt)
|
|
183
184
|
? medusaConfigOpt
|
|
184
185
|
: path.join(foundryRoot, medusaConfigOpt);
|
|
185
|
-
await (0, link_1.runLink)(foundryRoot, echidnaConfigPath, medusaConfigPath);
|
|
186
|
+
await (0, link_1.runLink)(foundryRoot, echidnaConfigPath, medusaConfigPath, !!opts.verbose);
|
|
186
187
|
});
|
|
187
188
|
program
|
|
188
189
|
.action(async (opts) => {
|
package/dist/link.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function runLink(foundryRoot: string, echidnaConfigPath: string, medusaConfigPath: string): Promise<void>;
|
|
1
|
+
export declare function runLink(foundryRoot: string, echidnaConfigPath: string, medusaConfigPath: string, verbose?: boolean): Promise<void>;
|
package/dist/link.js
CHANGED
|
@@ -44,9 +44,15 @@ const utils_1 = require("./utils");
|
|
|
44
44
|
const generateHexAddress = (index) => {
|
|
45
45
|
return `0xf${(index + 1).toString().padStart(2, '0')}`;
|
|
46
46
|
};
|
|
47
|
-
const parseLibrariesFromOutput = (output) => {
|
|
47
|
+
const parseLibrariesFromOutput = (output, verbose = false) => {
|
|
48
|
+
if (verbose) {
|
|
49
|
+
console.log(`[VERBOSE] Parsing libraries from output (${output.length} chars)`);
|
|
50
|
+
}
|
|
48
51
|
const usesPattern = /^\s+uses: \[(.*?)\]/gm;
|
|
49
52
|
const matches = [...(0, utils_1.stripAnsiCodes)(output).matchAll(usesPattern)];
|
|
53
|
+
if (verbose) {
|
|
54
|
+
console.log(`[VERBOSE] Found ${matches.length} 'uses:' patterns`);
|
|
55
|
+
}
|
|
50
56
|
const allLibraries = [];
|
|
51
57
|
for (const match of matches) {
|
|
52
58
|
if (match[1]) {
|
|
@@ -54,6 +60,9 @@ const parseLibrariesFromOutput = (output) => {
|
|
|
54
60
|
.split(',')
|
|
55
61
|
.map(lib => lib.trim().replace(/["'\s]/g, ''))
|
|
56
62
|
.filter(lib => lib.length > 0);
|
|
63
|
+
if (verbose) {
|
|
64
|
+
console.log(`[VERBOSE] Parsed libraries from match: ${libs.join(', ')}`);
|
|
65
|
+
}
|
|
57
66
|
for (const lib of libs) {
|
|
58
67
|
if (!allLibraries.includes(lib)) {
|
|
59
68
|
allLibraries.push(lib);
|
|
@@ -63,11 +72,21 @@ const parseLibrariesFromOutput = (output) => {
|
|
|
63
72
|
}
|
|
64
73
|
return allLibraries;
|
|
65
74
|
};
|
|
66
|
-
const runCryticCompile = (cwd) => {
|
|
75
|
+
const runCryticCompile = (cwd, verbose = false) => {
|
|
67
76
|
return new Promise((resolve, reject) => {
|
|
68
|
-
|
|
77
|
+
const cmd = 'crytic-compile . --foundry-compile-all --print-libraries';
|
|
78
|
+
if (verbose) {
|
|
79
|
+
console.log(`[VERBOSE] Running command: ${cmd}`);
|
|
80
|
+
console.log(`[VERBOSE] Working directory: ${cwd}`);
|
|
81
|
+
}
|
|
82
|
+
(0, child_process_1.exec)(cmd, { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (error, stdout, stderr) => {
|
|
83
|
+
if (verbose) {
|
|
84
|
+
console.log(`[VERBOSE] stdout:\n${stdout}`);
|
|
85
|
+
console.log(`[VERBOSE] stderr:\n${stderr}`);
|
|
86
|
+
}
|
|
69
87
|
if (error) {
|
|
70
|
-
|
|
88
|
+
const errorMsg = `crytic-compile failed with exit code ${error.code}\nstderr: ${stderr}\nstdout: ${stdout}\nerror: ${error.message}`;
|
|
89
|
+
reject(new Error(errorMsg));
|
|
71
90
|
return;
|
|
72
91
|
}
|
|
73
92
|
resolve(stdout || '');
|
|
@@ -92,13 +111,22 @@ const formatEchidnaYaml = (config, libraries) => {
|
|
|
92
111
|
});
|
|
93
112
|
return out;
|
|
94
113
|
};
|
|
95
|
-
const updateEchidnaConfig = async (configPath, libraries) => {
|
|
114
|
+
const updateEchidnaConfig = async (configPath, libraries, verbose = false) => {
|
|
115
|
+
if (verbose) {
|
|
116
|
+
console.log(`[VERBOSE] Reading echidna config from: ${configPath}`);
|
|
117
|
+
}
|
|
96
118
|
const content = await fs.readFile(configPath, 'utf8');
|
|
97
119
|
const parsed = yaml_1.default.parse(content) || {};
|
|
98
120
|
const updated = formatEchidnaYaml(parsed, libraries);
|
|
121
|
+
if (verbose) {
|
|
122
|
+
console.log(`[VERBOSE] Updated echidna config:\n${updated}`);
|
|
123
|
+
}
|
|
99
124
|
await fs.writeFile(configPath, updated, 'utf8');
|
|
100
125
|
};
|
|
101
|
-
const updateMedusaConfig = async (configPath, libraries) => {
|
|
126
|
+
const updateMedusaConfig = async (configPath, libraries, verbose = false) => {
|
|
127
|
+
if (verbose) {
|
|
128
|
+
console.log(`[VERBOSE] Reading medusa config from: ${configPath}`);
|
|
129
|
+
}
|
|
102
130
|
const content = await fs.readFile(configPath, 'utf8');
|
|
103
131
|
const parsed = JSON.parse(content);
|
|
104
132
|
if (!parsed.compilation)
|
|
@@ -112,26 +140,50 @@ const updateMedusaConfig = async (configPath, libraries) => {
|
|
|
112
140
|
else {
|
|
113
141
|
delete parsed.compilation.platformConfig.args;
|
|
114
142
|
}
|
|
143
|
+
if (verbose) {
|
|
144
|
+
console.log(`[VERBOSE] Updated medusa config:\n${JSON.stringify(parsed, null, 2)}`);
|
|
145
|
+
}
|
|
115
146
|
await fs.writeFile(configPath, JSON.stringify(parsed, null, 2), 'utf8');
|
|
116
147
|
};
|
|
117
|
-
async function runLink(foundryRoot, echidnaConfigPath, medusaConfigPath) {
|
|
118
|
-
|
|
119
|
-
|
|
148
|
+
async function runLink(foundryRoot, echidnaConfigPath, medusaConfigPath, verbose = false) {
|
|
149
|
+
if (verbose) {
|
|
150
|
+
console.log(`[VERBOSE] Starting runLink`);
|
|
151
|
+
console.log(`[VERBOSE] Foundry root: ${foundryRoot}`);
|
|
152
|
+
console.log(`[VERBOSE] Echidna config path: ${echidnaConfigPath}`);
|
|
153
|
+
console.log(`[VERBOSE] Medusa config path: ${medusaConfigPath}`);
|
|
154
|
+
}
|
|
155
|
+
let output;
|
|
156
|
+
try {
|
|
157
|
+
output = await runCryticCompile(foundryRoot, verbose);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
throw new Error(`crytic-compile execution failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
161
|
+
}
|
|
162
|
+
const libraries = parseLibrariesFromOutput(output, verbose);
|
|
120
163
|
console.log('Detected libraries:', libraries.length ? libraries.join(', ') : '(none)');
|
|
121
164
|
try {
|
|
122
165
|
await fs.access(echidnaConfigPath);
|
|
123
|
-
|
|
166
|
+
if (verbose) {
|
|
167
|
+
console.log(`[VERBOSE] Echidna config exists, updating...`);
|
|
168
|
+
}
|
|
169
|
+
await updateEchidnaConfig(echidnaConfigPath, libraries, verbose);
|
|
124
170
|
console.log(`Updated echidna config at ${echidnaConfigPath}`);
|
|
125
171
|
}
|
|
126
172
|
catch (e) {
|
|
127
|
-
throw new Error(`Failed to update echidna config: ${e}`);
|
|
173
|
+
throw new Error(`Failed to update echidna config: ${e instanceof Error ? e.message : String(e)}`);
|
|
128
174
|
}
|
|
129
175
|
try {
|
|
130
176
|
await fs.access(medusaConfigPath);
|
|
131
|
-
|
|
177
|
+
if (verbose) {
|
|
178
|
+
console.log(`[VERBOSE] Medusa config exists, updating...`);
|
|
179
|
+
}
|
|
180
|
+
await updateMedusaConfig(medusaConfigPath, libraries, verbose);
|
|
132
181
|
console.log(`Updated medusa config at ${medusaConfigPath}`);
|
|
133
182
|
}
|
|
134
183
|
catch (e) {
|
|
135
|
-
throw new Error(`Failed to update medusa config: ${e}`);
|
|
184
|
+
throw new Error(`Failed to update medusa config: ${e instanceof Error ? e.message : String(e)}`);
|
|
185
|
+
}
|
|
186
|
+
if (verbose) {
|
|
187
|
+
console.log(`[VERBOSE] runLink completed successfully`);
|
|
136
188
|
}
|
|
137
189
|
}
|
package/dist/processor.js
CHANGED
|
@@ -107,21 +107,28 @@ const matchesSignature = (sig, allowed) => {
|
|
|
107
107
|
return true;
|
|
108
108
|
return false;
|
|
109
109
|
};
|
|
110
|
-
const processFunction = (fnDef, includeDeps = false) => {
|
|
110
|
+
const processFunction = (fnDef, includeDeps = false, visited = new Set()) => {
|
|
111
111
|
const result = [];
|
|
112
|
+
if (visited.has(fnDef.id)) {
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
visited.add(fnDef.id);
|
|
112
116
|
fnDef.walk((n) => {
|
|
113
117
|
if ('vReferencedDeclaration' in n &&
|
|
114
118
|
n.vReferencedDeclaration &&
|
|
115
119
|
n.vReferencedDeclaration !== fnDef) {
|
|
116
120
|
if (n.vReferencedDeclaration instanceof $.FunctionDefinition ||
|
|
117
121
|
n.vReferencedDeclaration instanceof $.ModifierDefinition) {
|
|
118
|
-
|
|
122
|
+
if (visited.has(n.vReferencedDeclaration.id)) {
|
|
123
|
+
return; // break potential recursion cycles
|
|
124
|
+
}
|
|
119
125
|
if (result.some((x) => x.ast === n.vReferencedDeclaration)) {
|
|
120
126
|
return;
|
|
121
127
|
}
|
|
128
|
+
const nextVisited = new Set(visited);
|
|
122
129
|
result.push({
|
|
123
130
|
ast: n.vReferencedDeclaration,
|
|
124
|
-
children: processFunction(n.vReferencedDeclaration, includeDeps),
|
|
131
|
+
children: processFunction(n.vReferencedDeclaration, includeDeps, nextVisited),
|
|
125
132
|
callType: n instanceof $.FunctionCall ? (0, utils_1.getCallType)(n) : types_1.CallType.Internal,
|
|
126
133
|
});
|
|
127
134
|
}
|
|
@@ -165,14 +172,130 @@ const skipPatterns = [
|
|
|
165
172
|
'lib/chimera/',
|
|
166
173
|
'lib/setup-helpers/',
|
|
167
174
|
];
|
|
175
|
+
/**
|
|
176
|
+
* Normalizes a path by stripping common prefixes like /recon/ that some servers add.
|
|
177
|
+
* This ensures consistent path handling across different environments.
|
|
178
|
+
*/
|
|
179
|
+
const normalizePath = (relPath) => {
|
|
180
|
+
// Strip leading /recon/ or recon/ prefix if present
|
|
181
|
+
let normalized = relPath.replace(/^\/?(recon)\//i, '');
|
|
182
|
+
// Ensure no leading slash for relative paths
|
|
183
|
+
normalized = normalized.replace(/^\/+/, '');
|
|
184
|
+
return normalized;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Checks if a line should be excluded from coverage.
|
|
188
|
+
* Lines that Echidna cannot report on:
|
|
189
|
+
* - Empty lines or whitespace-only lines
|
|
190
|
+
* - Comment-only lines (// ...) but NOT lines with code before comment
|
|
191
|
+
* - Lines with only "}"
|
|
192
|
+
* - Lines with only "});"
|
|
193
|
+
* - Lines with only "} else {"
|
|
194
|
+
*/
|
|
195
|
+
const isUncoverableLine = (line) => {
|
|
196
|
+
const trimmed = line.trim();
|
|
197
|
+
// Empty or whitespace-only
|
|
198
|
+
if (trimmed === '')
|
|
199
|
+
return true;
|
|
200
|
+
// Comment-only line (starts with //)
|
|
201
|
+
if (/^\/\//.test(trimmed))
|
|
202
|
+
return true;
|
|
203
|
+
// Block comment only line
|
|
204
|
+
if (/^\/\*.*\*\/$/.test(trimmed))
|
|
205
|
+
return true;
|
|
206
|
+
if (/^\*/.test(trimmed))
|
|
207
|
+
return true; // middle of block comment
|
|
208
|
+
if (/^\/\*/.test(trimmed) && !/\*\//.test(trimmed))
|
|
209
|
+
return true; // start of multi-line block comment
|
|
210
|
+
// Lines with only closing braces/brackets
|
|
211
|
+
if (trimmed === '}')
|
|
212
|
+
return true;
|
|
213
|
+
if (trimmed === '});')
|
|
214
|
+
return true;
|
|
215
|
+
if (trimmed === ')')
|
|
216
|
+
return true;
|
|
217
|
+
if (trimmed === ');')
|
|
218
|
+
return true;
|
|
219
|
+
if (trimmed === '_;')
|
|
220
|
+
return true;
|
|
221
|
+
if (trimmed === '} else {')
|
|
222
|
+
return true;
|
|
223
|
+
if (trimmed === '} else if')
|
|
224
|
+
return true;
|
|
225
|
+
if (/^\}\s*else\s*\{$/.test(trimmed))
|
|
226
|
+
return true;
|
|
227
|
+
if (/^\}\s*else\s+if\s*\(/.test(trimmed))
|
|
228
|
+
return true;
|
|
229
|
+
// Unchecked blocks
|
|
230
|
+
if (trimmed === 'unchecked {')
|
|
231
|
+
return true;
|
|
232
|
+
if (/^unchecked\s*\{$/.test(trimmed))
|
|
233
|
+
return true;
|
|
234
|
+
return false;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Filters a range of lines, removing uncoverable lines and splitting into valid sub-ranges.
|
|
238
|
+
* @param lines Array of all lines in the file (0-indexed)
|
|
239
|
+
* @param startLine 1-based start line
|
|
240
|
+
* @param endLine 1-based end line
|
|
241
|
+
* @returns Array of range strings (e.g., ["5", "7-9", "12"])
|
|
242
|
+
*/
|
|
243
|
+
const filterLineRange = (lines, startLine, endLine) => {
|
|
244
|
+
var _a;
|
|
245
|
+
const result = [];
|
|
246
|
+
let rangeStart = null;
|
|
247
|
+
for (let lineNum = startLine; lineNum <= endLine; lineNum++) {
|
|
248
|
+
const lineIndex = lineNum - 1; // Convert to 0-based index
|
|
249
|
+
const line = (_a = lines[lineIndex]) !== null && _a !== void 0 ? _a : '';
|
|
250
|
+
if (isUncoverableLine(line)) {
|
|
251
|
+
// End current range if we have one
|
|
252
|
+
if (rangeStart !== null) {
|
|
253
|
+
const rangeEnd = lineNum - 1;
|
|
254
|
+
result.push(rangeStart === rangeEnd ? `${rangeStart}` : `${rangeStart}-${rangeEnd}`);
|
|
255
|
+
rangeStart = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Start new range or continue existing one
|
|
260
|
+
if (rangeStart === null) {
|
|
261
|
+
rangeStart = lineNum;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Close any remaining range
|
|
266
|
+
if (rangeStart !== null) {
|
|
267
|
+
result.push(rangeStart === endLine ? `${rangeStart}` : `${rangeStart}-${endLine}`);
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
};
|
|
168
271
|
async function buildCoverageMap(asts, foundryRoot, contractFunctions) {
|
|
272
|
+
var _a;
|
|
169
273
|
const coverage = new Map();
|
|
170
274
|
const fileCache = new Map();
|
|
275
|
+
const fileLinesCache = new Map();
|
|
171
276
|
let nodesVisited = 0;
|
|
172
277
|
let nodesAdded = 0;
|
|
173
278
|
const shouldSkipPath = (relPath) => {
|
|
174
279
|
return skipPatterns.some((p) => relPath.includes(p));
|
|
175
280
|
};
|
|
281
|
+
const getFileLines = async (absPath) => {
|
|
282
|
+
let lines = fileLinesCache.get(absPath);
|
|
283
|
+
if (!lines) {
|
|
284
|
+
let content = fileCache.get(absPath);
|
|
285
|
+
if (!content) {
|
|
286
|
+
try {
|
|
287
|
+
content = await fs.readFile(absPath, 'utf8');
|
|
288
|
+
fileCache.set(absPath, content);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
lines = content.split('\n');
|
|
295
|
+
fileLinesCache.set(absPath, lines);
|
|
296
|
+
}
|
|
297
|
+
return lines;
|
|
298
|
+
};
|
|
176
299
|
const addNodeRange = async (node) => {
|
|
177
300
|
var _a;
|
|
178
301
|
nodesVisited++;
|
|
@@ -239,14 +362,57 @@ async function buildCoverageMap(asts, foundryRoot, contractFunctions) {
|
|
|
239
362
|
}
|
|
240
363
|
const normalized = {};
|
|
241
364
|
for (const [relPath, ranges] of coverage.entries()) {
|
|
242
|
-
|
|
365
|
+
// Normalize the path to handle /recon/ prefix
|
|
366
|
+
const normalizedRelPath = normalizePath(relPath);
|
|
367
|
+
if (shouldSkipPath(normalizedRelPath)) {
|
|
243
368
|
continue; // skip test/script/lib files at the final emission step only
|
|
244
369
|
}
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
370
|
+
const absPath = path.isAbsolute(relPath)
|
|
371
|
+
? relPath
|
|
372
|
+
: path.join(foundryRoot, relPath);
|
|
373
|
+
const lines = await getFileLines(absPath);
|
|
374
|
+
// Expand all ranges, filter uncoverable lines, and collect valid line numbers
|
|
375
|
+
const validLines = new Set();
|
|
376
|
+
for (const range of ranges) {
|
|
377
|
+
const parts = range.split('-');
|
|
378
|
+
const start = parseInt(parts[0], 10);
|
|
379
|
+
const end = parts.length > 1 ? parseInt(parts[1], 10) : start;
|
|
380
|
+
for (let lineNum = start; lineNum <= end; lineNum++) {
|
|
381
|
+
const lineIndex = lineNum - 1;
|
|
382
|
+
const line = (_a = lines[lineIndex]) !== null && _a !== void 0 ? _a : '';
|
|
383
|
+
if (!isUncoverableLine(line)) {
|
|
384
|
+
validLines.add(lineNum);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Convert valid line numbers back to ranges
|
|
389
|
+
const sortedLines = Array.from(validLines).sort((a, b) => a - b);
|
|
390
|
+
const filteredRanges = [];
|
|
391
|
+
let rangeStart = null;
|
|
392
|
+
let rangeEnd = null;
|
|
393
|
+
for (const lineNum of sortedLines) {
|
|
394
|
+
if (rangeStart === null) {
|
|
395
|
+
rangeStart = lineNum;
|
|
396
|
+
rangeEnd = lineNum;
|
|
397
|
+
}
|
|
398
|
+
else if (lineNum === rangeEnd + 1) {
|
|
399
|
+
rangeEnd = lineNum;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// Emit previous range
|
|
403
|
+
filteredRanges.push(rangeStart === rangeEnd ? `${rangeStart}` : `${rangeStart}-${rangeEnd}`);
|
|
404
|
+
rangeStart = lineNum;
|
|
405
|
+
rangeEnd = lineNum;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// Emit final range
|
|
409
|
+
if (rangeStart !== null) {
|
|
410
|
+
filteredRanges.push(rangeStart === rangeEnd ? `${rangeStart}` : `${rangeStart}-${rangeEnd}`);
|
|
411
|
+
}
|
|
412
|
+
if (filteredRanges.length > 0) {
|
|
413
|
+
// Use normalized path as key for consistent output
|
|
414
|
+
normalized[normalizedRelPath] = filteredRanges;
|
|
415
|
+
}
|
|
250
416
|
}
|
|
251
417
|
return normalized;
|
|
252
418
|
}
|