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 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
- (0, child_process_1.exec)('crytic-compile . --foundry-compile-all --print-libraries', { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (error, stdout, stderr) => {
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
- reject(new Error(stderr || error.message));
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
- const output = await runCryticCompile(foundryRoot);
119
- const libraries = parseLibrariesFromOutput(output);
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
- await updateEchidnaConfig(echidnaConfigPath, libraries);
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
- await updateMedusaConfig(medusaConfigPath, libraries);
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
- const refSourceUnit = n.vReferencedDeclaration.getClosestParentByType($.SourceUnit);
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
- if (shouldSkipPath(relPath)) {
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 sorted = Array.from(ranges).sort((a, b) => {
246
- const start = (val) => parseInt(val.split('-')[0], 10);
247
- return start(a) - start(b);
248
- });
249
- normalized[relPath] = sorted;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recon-generate",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {