recon-generate 0.0.6 → 0.0.8

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.
@@ -0,0 +1,627 @@
1
+ "use strict";
2
+ /**
3
+ * Path Generator
4
+ *
5
+ * Generates minimal paths needed for 100% branch coverage.
6
+ * Output is optimized for LLM consumption.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.runPaths = void 0;
43
+ const child_process_1 = require("child_process");
44
+ const fs = __importStar(require("fs/promises"));
45
+ const path = __importStar(require("path"));
46
+ const solc_typed_ast_1 = require("solc-typed-ast");
47
+ const call_tree_builder_1 = require("./analyzer/call-tree-builder");
48
+ const utils_1 = require("./utils");
49
+ // Contracts to skip (test helpers)
50
+ const SKIP_CONTRACTS = new Set(['IHevm', 'Vm', 'StdChains', 'StdCheatsSafe', 'StdCheats']);
51
+ // ==================== Helpers ====================
52
+ const runCmd = (cmd, cwd) => {
53
+ return new Promise((resolve, reject) => {
54
+ (0, child_process_1.exec)(cmd, { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (err, _stdout, stderr) => {
55
+ if (err) {
56
+ reject(new Error(stderr || err.message));
57
+ }
58
+ else {
59
+ resolve();
60
+ }
61
+ });
62
+ });
63
+ };
64
+ const loadLatestSourceUnits = async (foundryRoot) => {
65
+ var _a;
66
+ const outDir = path.join(foundryRoot, '.recon', 'out');
67
+ const buildInfoDir = path.join(outDir, 'build-info');
68
+ let files = [];
69
+ try {
70
+ const entries = await fs.readdir(buildInfoDir);
71
+ const jsonFiles = entries.filter((f) => f.endsWith('.json'));
72
+ files = await Promise.all(jsonFiles.map(async (f) => ({
73
+ name: f,
74
+ path: path.join(buildInfoDir, f),
75
+ mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime,
76
+ })));
77
+ }
78
+ catch (e) {
79
+ throw new Error(`No build-info directory found at ${buildInfoDir}: ${e}`);
80
+ }
81
+ if (files.length === 0) {
82
+ throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
83
+ }
84
+ const latestFile = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path;
85
+ const fileContent = await fs.readFile(latestFile, 'utf-8');
86
+ const buildInfo = JSON.parse(fileContent);
87
+ const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
88
+ if (!buildOutput) {
89
+ throw new Error(`Build-info file ${latestFile} is missing output data.`);
90
+ }
91
+ const filteredAstData = { ...buildOutput };
92
+ if (filteredAstData.sources) {
93
+ const validSources = {};
94
+ for (const [key, content] of Object.entries(filteredAstData.sources)) {
95
+ const ast = content.ast || content.legacyAST || content.AST;
96
+ if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
97
+ validSources[key] = content;
98
+ }
99
+ }
100
+ filteredAstData.sources = validSources;
101
+ }
102
+ const reader = new solc_typed_ast_1.ASTReader();
103
+ return reader.read(filteredAstData);
104
+ };
105
+ // ==================== Main Entry ====================
106
+ const runPaths = async (foundryRoot, cryticName, suiteNameSnake) => {
107
+ const buildCmd = `forge build --contracts ${cryticName} --build-info --out .recon/out`.replace(/\s+/g, ' ').trim();
108
+ await runCmd(buildCmd, foundryRoot);
109
+ const sourceUnits = await loadLatestSourceUnits(foundryRoot);
110
+ if (!sourceUnits || sourceUnits.length === 0) {
111
+ throw new Error('No source units were produced from the Crytic build; cannot generate paths.');
112
+ }
113
+ // Find target contract
114
+ const targetContract = findContract(sourceUnits, cryticName);
115
+ if (!targetContract) {
116
+ throw new Error(`Contract ${cryticName} not found`);
117
+ }
118
+ // Get target functions (including inherited ones)
119
+ const allFunctions = (0, utils_1.getDefinitions)(targetContract, 'vFunctions');
120
+ const targetFunctions = allFunctions.filter(f => f.visibility === solc_typed_ast_1.FunctionVisibility.Public &&
121
+ f.implemented &&
122
+ !f.name.startsWith('_'));
123
+ // Process each function
124
+ const output = {};
125
+ let totalPaths = 0;
126
+ for (const func of targetFunctions) {
127
+ // Find external call
128
+ const externalCall = findExternalCall(func);
129
+ if (!externalCall)
130
+ continue;
131
+ // Build call tree for external function
132
+ const extFunc = findFunction(sourceUnits, externalCall.contract, externalCall.function);
133
+ if (!extFunc)
134
+ continue;
135
+ const extContract = findContract(sourceUnits, externalCall.contract);
136
+ if (!extContract)
137
+ continue;
138
+ // Build call tree context
139
+ const context = {
140
+ nodeCounter: { value: 0 },
141
+ contract: extContract,
142
+ callStack: [],
143
+ activeNodes: new Map()
144
+ };
145
+ const callTree = (0, call_tree_builder_1.buildCallTree)(extFunc, [], undefined, undefined, context);
146
+ // Build param mapping
147
+ const paramMapping = buildParamMapping(func, externalCall);
148
+ // Enumerate paths
149
+ const enumerator = new PathEnumerator(sourceUnits, paramMapping);
150
+ const paths = enumerator.enumerate(callTree);
151
+ if (paths.length === 0)
152
+ continue;
153
+ // Simple output - combined string
154
+ const simplified = simplifyPaths(paths);
155
+ // Skip if no real branches (only "true")
156
+ if (simplified.length === 1 && simplified[0] === 'true')
157
+ continue;
158
+ output[func.name] = simplified;
159
+ totalPaths += simplified.length;
160
+ }
161
+ const pathsName = suiteNameSnake ? `recon-${suiteNameSnake}-paths.json` : 'recon-paths.json';
162
+ const pathsFilePath = path.join(foundryRoot, pathsName);
163
+ await fs.writeFile(pathsFilePath, JSON.stringify(output, null, 2));
164
+ console.log(`[recon-generate] Wrote paths file to ${pathsFilePath}`);
165
+ };
166
+ exports.runPaths = runPaths;
167
+ // ==================== Source Helpers ====================
168
+ function findContract(units, name) {
169
+ for (const unit of units) {
170
+ for (const contract of unit.vContracts) {
171
+ if (contract.name === name)
172
+ return contract;
173
+ }
174
+ }
175
+ return null;
176
+ }
177
+ function findFunction(units, contractName, funcName) {
178
+ const contract = findContract(units, contractName);
179
+ if (!contract)
180
+ return null;
181
+ for (const func of contract.vFunctions) {
182
+ if (func.name === funcName && func.implemented)
183
+ return func;
184
+ }
185
+ // Check base contracts
186
+ for (const baseContract of contract.vLinearizedBaseContracts) {
187
+ if (baseContract.id === contract.id)
188
+ continue;
189
+ for (const f of baseContract.vFunctions) {
190
+ if (f.name === funcName && f.implemented)
191
+ return f;
192
+ }
193
+ }
194
+ return null;
195
+ }
196
+ function findExternalCall(func) {
197
+ if (!func.vBody)
198
+ return null;
199
+ const calls = func.vBody.getChildrenByType(solc_typed_ast_1.FunctionCall);
200
+ for (const call of calls) {
201
+ const expr = call.vExpression;
202
+ if (expr instanceof solc_typed_ast_1.MemberAccess) {
203
+ const baseType = expr.vExpression.typeString || '';
204
+ if (baseType.startsWith('contract ')) {
205
+ const match = baseType.match(/contract\s+(\w+)/);
206
+ if (match && !SKIP_CONTRACTS.has(match[1])) {
207
+ return {
208
+ contract: match[1],
209
+ function: expr.memberName,
210
+ args: call.vArguments
211
+ };
212
+ }
213
+ }
214
+ }
215
+ }
216
+ return null;
217
+ }
218
+ function buildParamMapping(func, extCall) {
219
+ const mapping = new Map();
220
+ for (const param of func.vParameters.vParameters) {
221
+ mapping.set(param.name, param.name);
222
+ }
223
+ return mapping;
224
+ }
225
+ // ==================== Path Enumeration ====================
226
+ class PathEnumerator {
227
+ constructor(sourceUnits, paramMap) {
228
+ this.pathIdCounter = 0;
229
+ this.branchPoints = 0;
230
+ this.sourceUnits = sourceUnits;
231
+ this.paramMap = new Map(paramMap);
232
+ }
233
+ enumerate(callTree) {
234
+ this.pathIdCounter = 0;
235
+ this.branchPoints = 0;
236
+ const paths = this.processCallTree(callTree);
237
+ return paths
238
+ .filter(p => p.result === 'success')
239
+ .map(p => ({ id: p.id, conditions: p.conditions, requires: p.requires, result: p.result }));
240
+ }
241
+ processCallTree(node) {
242
+ const def = node.definition;
243
+ // Process children first (leaf to root)
244
+ const childPathsMap = new Map();
245
+ for (const child of node.children) {
246
+ if (child.fnCall instanceof solc_typed_ast_1.ModifierInvocation)
247
+ continue;
248
+ const savedMap = new Map(this.paramMap);
249
+ this.updateParamMap(child);
250
+ const childPaths = this.processCallTree(child);
251
+ childPathsMap.set(child.nodeId, childPaths);
252
+ this.paramMap = savedMap;
253
+ }
254
+ // Process function body
255
+ let activePaths = [{
256
+ id: ++this.pathIdCounter,
257
+ conditions: [],
258
+ requires: [],
259
+ terminated: false
260
+ }];
261
+ if (def.vBody) {
262
+ activePaths = this.walkBlock(def.vBody, activePaths, node, childPathsMap);
263
+ }
264
+ // Mark non-terminated as success
265
+ for (const p of activePaths) {
266
+ if (!p.terminated) {
267
+ p.terminated = true;
268
+ p.result = 'success';
269
+ }
270
+ }
271
+ return activePaths;
272
+ }
273
+ updateParamMap(node) {
274
+ const params = node.definition.vParameters.vParameters;
275
+ const args = node.callArgs;
276
+ for (let i = 0; i < params.length && i < args.length; i++) {
277
+ if (params[i].name && args[i]) {
278
+ this.paramMap.set(params[i].name, this.resolveExpr(args[i]));
279
+ }
280
+ }
281
+ }
282
+ walkBlock(block, paths, node, childPaths) {
283
+ for (const stmt of block.vStatements) {
284
+ if (paths.every(p => p.terminated))
285
+ break;
286
+ paths = this.processStatement(stmt, paths, node, childPaths);
287
+ }
288
+ return paths;
289
+ }
290
+ processStatement(stmt, paths, node, childPaths) {
291
+ if (stmt instanceof solc_typed_ast_1.IfStatement) {
292
+ return this.handleIf(stmt, paths, node, childPaths);
293
+ }
294
+ if (stmt instanceof solc_typed_ast_1.TryStatement) {
295
+ return this.handleTryCatch(stmt, paths, node, childPaths);
296
+ }
297
+ if (stmt instanceof solc_typed_ast_1.Return) {
298
+ for (const p of paths)
299
+ if (!p.terminated) {
300
+ p.terminated = true;
301
+ p.result = 'success';
302
+ }
303
+ return paths;
304
+ }
305
+ if (stmt instanceof solc_typed_ast_1.RevertStatement) {
306
+ for (const p of paths)
307
+ if (!p.terminated) {
308
+ p.terminated = true;
309
+ p.result = 'revert';
310
+ }
311
+ return paths;
312
+ }
313
+ if (stmt instanceof solc_typed_ast_1.Block || stmt instanceof solc_typed_ast_1.UncheckedBlock) {
314
+ return this.walkBlock(stmt, paths, node, childPaths);
315
+ }
316
+ // Check for require/assert
317
+ paths = this.checkRequire(stmt, paths);
318
+ // Check for external calls (add as constraint - must succeed)
319
+ paths = this.checkExternalCalls(stmt, paths);
320
+ // Check for internal calls
321
+ paths = this.checkInternalCalls(stmt, paths, node, childPaths);
322
+ return paths;
323
+ }
324
+ handleTryCatch(stmt, paths, node, childPaths) {
325
+ this.branchPoints++;
326
+ // Get the external call expression
327
+ const extCall = stmt.vExternalCall;
328
+ const callExpr = this.formatExternalCall(extCall);
329
+ const results = [];
330
+ for (const path of paths) {
331
+ if (path.terminated) {
332
+ results.push(path);
333
+ continue;
334
+ }
335
+ // Process each clause
336
+ for (const clause of stmt.vClauses) {
337
+ if (clause.errorName === '') {
338
+ // Success clause (try block) - external call succeeds
339
+ const successPath = {
340
+ id: ++this.pathIdCounter,
341
+ conditions: [...path.conditions, {
342
+ original: callExpr,
343
+ resolved: callExpr,
344
+ mustBeTrue: true
345
+ }],
346
+ requires: [...path.requires],
347
+ terminated: false
348
+ };
349
+ const successResults = this.walkBlock(clause.vBlock, [successPath], node, childPaths);
350
+ results.push(...successResults);
351
+ }
352
+ else {
353
+ // Catch clause - external call fails
354
+ const catchPath = {
355
+ id: ++this.pathIdCounter,
356
+ conditions: [...path.conditions, {
357
+ original: callExpr,
358
+ resolved: callExpr,
359
+ mustBeTrue: false // negated - call failed
360
+ }],
361
+ requires: [...path.requires],
362
+ terminated: false
363
+ };
364
+ const catchResults = this.walkBlock(clause.vBlock, [catchPath], node, childPaths);
365
+ results.push(...catchResults);
366
+ }
367
+ }
368
+ }
369
+ return results;
370
+ }
371
+ checkExternalCalls(stmt, paths) {
372
+ var _a, _b;
373
+ // Find all function calls in this statement
374
+ const calls = ((_b = (_a = stmt).getChildrenByType) === null || _b === void 0 ? void 0 : _b.call(_a, solc_typed_ast_1.FunctionCall)) || [];
375
+ for (const call of calls) {
376
+ // Check if it's a high-level external call
377
+ if ((0, utils_1.highLevelCall)(call)) {
378
+ const callExpr = this.formatExternalCall(call);
379
+ // Add as a constraint - external call must succeed
380
+ for (const p of paths) {
381
+ if (!p.terminated) {
382
+ p.requires.push({
383
+ original: callExpr,
384
+ resolved: callExpr,
385
+ mustBeTrue: true
386
+ });
387
+ }
388
+ }
389
+ }
390
+ }
391
+ return paths;
392
+ }
393
+ formatExternalCall(call) {
394
+ if (call.vExpression instanceof solc_typed_ast_1.MemberAccess) {
395
+ const base = call.vExpression.vExpression;
396
+ const func = call.vExpression.memberName;
397
+ const baseStr = this.resolveExpr(base);
398
+ const args = call.vArguments.map(a => this.resolveExpr(a)).join(', ');
399
+ return `${baseStr}.${func}(${args})`;
400
+ }
401
+ return this.safeToSource(call);
402
+ }
403
+ handleIf(stmt, paths, node, childPaths) {
404
+ this.branchPoints++;
405
+ const condOrig = this.safeToSource(stmt.vCondition);
406
+ const condResolved = this.resolveExpr(stmt.vCondition);
407
+ const results = [];
408
+ for (const path of paths) {
409
+ if (path.terminated) {
410
+ results.push(path);
411
+ continue;
412
+ }
413
+ // True branch
414
+ const truePath = {
415
+ id: ++this.pathIdCounter,
416
+ conditions: [...path.conditions, { original: condOrig, resolved: condResolved, mustBeTrue: true }],
417
+ requires: [...path.requires],
418
+ terminated: false
419
+ };
420
+ if (stmt.vTrueBody) {
421
+ const trueResults = stmt.vTrueBody instanceof solc_typed_ast_1.Block || stmt.vTrueBody instanceof solc_typed_ast_1.UncheckedBlock
422
+ ? this.walkBlock(stmt.vTrueBody, [truePath], node, childPaths)
423
+ : this.processStatement(stmt.vTrueBody, [truePath], node, childPaths);
424
+ results.push(...trueResults);
425
+ }
426
+ else {
427
+ results.push(truePath);
428
+ }
429
+ // False branch
430
+ const falsePath = {
431
+ id: ++this.pathIdCounter,
432
+ conditions: [...path.conditions, { original: condOrig, resolved: condResolved, mustBeTrue: false }],
433
+ requires: [...path.requires],
434
+ terminated: false
435
+ };
436
+ if (stmt.vFalseBody) {
437
+ const falseResults = stmt.vFalseBody instanceof solc_typed_ast_1.Block || stmt.vFalseBody instanceof solc_typed_ast_1.UncheckedBlock
438
+ ? this.walkBlock(stmt.vFalseBody, [falsePath], node, childPaths)
439
+ : this.processStatement(stmt.vFalseBody, [falsePath], node, childPaths);
440
+ results.push(...falseResults);
441
+ }
442
+ else {
443
+ results.push(falsePath);
444
+ }
445
+ }
446
+ return results;
447
+ }
448
+ checkRequire(stmt, paths) {
449
+ var _a, _b;
450
+ const calls = ((_b = (_a = stmt).getChildrenByType) === null || _b === void 0 ? void 0 : _b.call(_a, solc_typed_ast_1.FunctionCall)) || [];
451
+ for (const call of calls) {
452
+ const expr = call.vExpression;
453
+ if (expr instanceof solc_typed_ast_1.Identifier && (expr.name === 'require' || expr.name === 'assert')) {
454
+ if (call.vArguments.length > 0) {
455
+ const cond = call.vArguments[0];
456
+ const condOrig = this.safeToSource(cond);
457
+ const condResolved = this.resolveExpr(cond);
458
+ for (const p of paths) {
459
+ if (!p.terminated) {
460
+ p.requires.push({ original: condOrig, resolved: condResolved, mustBeTrue: true });
461
+ }
462
+ }
463
+ }
464
+ }
465
+ }
466
+ return paths;
467
+ }
468
+ checkInternalCalls(stmt, paths, node, childPaths) {
469
+ var _a, _b;
470
+ const calls = ((_b = (_a = stmt).getChildrenByType) === null || _b === void 0 ? void 0 : _b.call(_a, solc_typed_ast_1.FunctionCall)) || [];
471
+ for (const call of calls) {
472
+ for (const child of node.children) {
473
+ if (child.fnCall instanceof solc_typed_ast_1.ModifierInvocation)
474
+ continue;
475
+ if (child.fnCall.id === call.id && childPaths.has(child.nodeId)) {
476
+ const cPaths = childPaths.get(child.nodeId);
477
+ const merged = [];
478
+ for (const p of paths) {
479
+ if (p.terminated) {
480
+ merged.push(p);
481
+ continue;
482
+ }
483
+ for (const cp of cPaths) {
484
+ if (cp.result === 'revert')
485
+ continue;
486
+ merged.push({
487
+ id: ++this.pathIdCounter,
488
+ conditions: [...p.conditions, ...cp.conditions],
489
+ requires: [...p.requires, ...cp.requires],
490
+ terminated: false
491
+ });
492
+ }
493
+ }
494
+ if (merged.length > 0)
495
+ paths = merged;
496
+ }
497
+ }
498
+ }
499
+ return paths;
500
+ }
501
+ resolveExpr(expr) {
502
+ if (expr instanceof solc_typed_ast_1.Literal)
503
+ return expr.value || '0';
504
+ if (expr instanceof solc_typed_ast_1.Identifier) {
505
+ const name = expr.name;
506
+ if (this.paramMap.has(name))
507
+ return this.paramMap.get(name);
508
+ // State variables - no prefix needed, context makes it clear
509
+ return name;
510
+ }
511
+ if (expr instanceof solc_typed_ast_1.MemberAccess) {
512
+ const base = expr.vExpression;
513
+ if (base instanceof solc_typed_ast_1.Identifier && ['msg', 'block', 'tx'].includes(base.name)) {
514
+ return `${base.name}.${expr.memberName}`;
515
+ }
516
+ return `${this.resolveExpr(base)}.${expr.memberName}`;
517
+ }
518
+ if (expr instanceof solc_typed_ast_1.IndexAccess) {
519
+ const base = this.resolveExpr(expr.vBaseExpression);
520
+ const idx = expr.vIndexExpression ? this.resolveExpr(expr.vIndexExpression) : '0';
521
+ return `${base}[${idx}]`;
522
+ }
523
+ if (expr instanceof solc_typed_ast_1.BinaryOperation) {
524
+ const left = this.resolveExpr(expr.vLeftExpression);
525
+ const right = this.resolveExpr(expr.vRightExpression);
526
+ return `(${left} ${expr.operator} ${right})`;
527
+ }
528
+ if (expr instanceof solc_typed_ast_1.UnaryOperation) {
529
+ const sub = this.resolveExpr(expr.vSubExpression);
530
+ return expr.prefix ? `${expr.operator}${sub}` : `${sub}${expr.operator}`;
531
+ }
532
+ if (expr instanceof solc_typed_ast_1.FunctionCall && expr.kind === solc_typed_ast_1.FunctionCallKind.TypeConversion && expr.vArguments.length > 0) {
533
+ return this.resolveExpr(expr.vArguments[0]);
534
+ }
535
+ return this.safeToSource(expr);
536
+ }
537
+ safeToSource(node) {
538
+ try {
539
+ return (0, utils_1.toSource)(node);
540
+ }
541
+ catch {
542
+ return String(node);
543
+ }
544
+ }
545
+ }
546
+ // ==================== Path Simplification ====================
547
+ function simplifyPaths(paths) {
548
+ if (paths.length === 0)
549
+ return [];
550
+ // Extract unique branches
551
+ const branches = new Map();
552
+ for (const p of paths) {
553
+ for (const c of p.conditions) {
554
+ branches.set(c.resolved, true);
555
+ }
556
+ }
557
+ // Find minimum covering set (greedy)
558
+ const needToCover = new Set();
559
+ for (const key of branches.keys()) {
560
+ needToCover.add(`T:${key}`);
561
+ needToCover.add(`F:${key}`);
562
+ }
563
+ const selected = [];
564
+ const covered = new Set();
565
+ while (covered.size < needToCover.size) {
566
+ let bestPath = null;
567
+ let bestNewCoverage = 0;
568
+ for (const path of paths) {
569
+ if (selected.includes(path))
570
+ continue;
571
+ let newCoverage = 0;
572
+ for (const c of path.conditions) {
573
+ const key = c.mustBeTrue ? `T:${c.resolved}` : `F:${c.resolved}`;
574
+ if (needToCover.has(key) && !covered.has(key))
575
+ newCoverage++;
576
+ }
577
+ if (newCoverage > bestNewCoverage) {
578
+ bestNewCoverage = newCoverage;
579
+ bestPath = path;
580
+ }
581
+ }
582
+ if (!bestPath || bestNewCoverage === 0)
583
+ break;
584
+ selected.push(bestPath);
585
+ for (const c of bestPath.conditions) {
586
+ covered.add(c.mustBeTrue ? `T:${c.resolved}` : `F:${c.resolved}`);
587
+ }
588
+ }
589
+ if (selected.length === 0)
590
+ selected.push(paths[0]);
591
+ // Format as strings - include both conditions and external call requires
592
+ return selected.map(p => {
593
+ const conds = p.conditions.map(c => {
594
+ const clean = c.resolved.replace(/storage\./g, '').replace(/^\((.+)\)$/, '$1');
595
+ return c.mustBeTrue ? clean : negateCond(clean);
596
+ });
597
+ // Add external call requires (detect by pattern: has dot and parentheses)
598
+ // Filter out SafeCast overflow checks (noise)
599
+ const extCalls = p.requires
600
+ .filter(r => r.resolved.includes('.') && r.resolved.includes('('))
601
+ .filter(r => !isOverflowCheck(r.resolved))
602
+ .map(r => r.mustBeTrue ? r.resolved : `!${r.resolved}`);
603
+ const allConds = [...conds, ...extCalls];
604
+ return allConds.join(' && ') || 'true';
605
+ });
606
+ }
607
+ // Detect SafeCast overflow guards like (x <= type(uint128).max)
608
+ function isOverflowCheck(s) {
609
+ return /<=\s*type\(u?int\d+\)\.max/.test(s);
610
+ }
611
+ function negateCond(cond) {
612
+ if (cond.startsWith('!'))
613
+ return cond.slice(1);
614
+ if (cond.includes('=='))
615
+ return cond.replace('==', '!=');
616
+ if (cond.includes('!='))
617
+ return cond.replace('!=', '==');
618
+ if (cond.includes('>='))
619
+ return cond.replace('>=', '<');
620
+ if (cond.includes('<='))
621
+ return cond.replace('<=', '>');
622
+ if (cond.includes('>') && !cond.includes('>='))
623
+ return cond.replace('>', '<=');
624
+ if (cond.includes('<') && !cond.includes('<='))
625
+ return cond.replace('<', '>=');
626
+ return `!(${cond})`;
627
+ }
package/dist/processor.js CHANGED
@@ -153,11 +153,26 @@ const processContract = (contract) => {
153
153
  return result;
154
154
  };
155
155
  exports.processContract = processContract;
156
+ const skipPatterns = [
157
+ 'test/',
158
+ 'tests/',
159
+ 'script/',
160
+ 'scripts/',
161
+ 'contracts/test/',
162
+ 'contracts/script/',
163
+ 'contracts/scripts/',
164
+ 'lib/forge-std/',
165
+ 'lib/chimera/',
166
+ 'lib/setup-helpers/',
167
+ ];
156
168
  async function buildCoverageMap(asts, foundryRoot, contractFunctions) {
157
169
  const coverage = new Map();
158
170
  const fileCache = new Map();
159
171
  let nodesVisited = 0;
160
172
  let nodesAdded = 0;
173
+ const shouldSkipPath = (relPath) => {
174
+ return skipPatterns.some((p) => relPath.includes(p));
175
+ };
161
176
  const addNodeRange = async (node) => {
162
177
  var _a;
163
178
  nodesVisited++;
@@ -224,6 +239,9 @@ async function buildCoverageMap(asts, foundryRoot, contractFunctions) {
224
239
  }
225
240
  const normalized = {};
226
241
  for (const [relPath, ranges] of coverage.entries()) {
242
+ if (shouldSkipPath(relPath)) {
243
+ continue; // skip test/script/lib files at the final emission step only
244
+ }
227
245
  const sorted = Array.from(ranges).sort((a, b) => {
228
246
  const start = (val) => parseInt(val.split('-')[0], 10);
229
247
  return start(a) - start(b);
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ASTNode } from "solc-typed-ast";
1
+ import { ASTNode, Expression, FunctionCall, FunctionDefinition, ModifierDefinition, ModifierInvocation, Statement } from "solc-typed-ast";
2
2
  export declare enum Actor {
3
3
  ACTOR = "actor",
4
4
  ADMIN = "admin"
@@ -54,3 +54,13 @@ export type RecordItem = {
54
54
  children: RecordItem[];
55
55
  callType?: CallType;
56
56
  };
57
+ export type CallTree = {
58
+ nodeId: number;
59
+ definition: FunctionDefinition | ModifierDefinition;
60
+ fnCall?: FunctionCall | ModifierInvocation;
61
+ callArgs: Expression[];
62
+ callContext?: Statement;
63
+ children: CallTree[];
64
+ isRecursive?: boolean;
65
+ recursiveTargetNodeId?: number;
66
+ };