recon-generate 0.0.9 → 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.
Files changed (2) hide show
  1. package/dist/processor.js +175 -9
  2. package/package.json +1 -1
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.9",
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": {