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.
package/README.md CHANGED
@@ -69,6 +69,9 @@ recon-generate --mock "Morpho,Other" --force
69
69
  # Enable dynamic deploy helpers for Foo and Bar
70
70
  recon-generate --dynamic-deploy "Foo,Bar" --force
71
71
 
72
+ # Generate coverage JSON from a Crytic tester (no scaffolding)
73
+ recon-generate coverage --crytic-name CryticTester --name foo
74
+
72
75
  # Link subcommand
73
76
 
74
77
  After generating a suite you can update Echidna and Medusa configs with detected Foundry libraries:
@@ -0,0 +1,14 @@
1
+ import { ContractDefinition, Expression, FunctionCall, FunctionDefinition, ModifierDefinition, ModifierInvocation, Statement } from 'solc-typed-ast';
2
+ import { CallTree } from '../types';
3
+ export interface CallTreeBuilderContext {
4
+ nodeCounter: {
5
+ value: number;
6
+ };
7
+ contract: ContractDefinition;
8
+ callStack: number[];
9
+ activeNodes: Map<number, number>;
10
+ }
11
+ /**
12
+ * Build a call tree for a function or modifier
13
+ */
14
+ export declare function buildCallTree(currentFunc: FunctionDefinition | ModifierDefinition, callArgs?: Expression[], fnCall?: FunctionCall | ModifierInvocation, callContext?: Statement, context?: CallTreeBuilderContext): CallTree;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildCallTree = buildCallTree;
4
+ const solc_typed_ast_1 = require("solc-typed-ast");
5
+ const expression_analyzer_1 = require("./expression-analyzer");
6
+ /**
7
+ * Build a call tree for a function or modifier
8
+ */
9
+ function buildCallTree(currentFunc, callArgs = [], fnCall, callContext, context) {
10
+ if (!context) {
11
+ throw new Error('CallTreeBuilderContext is required');
12
+ }
13
+ const functionAstId = currentFunc.id;
14
+ const isRecursiveCall = context.callStack.includes(functionAstId);
15
+ const recursiveTargetNodeId = context.activeNodes.get(functionAstId);
16
+ const node = {
17
+ nodeId: context.nodeCounter.value++,
18
+ definition: currentFunc,
19
+ fnCall: fnCall,
20
+ callArgs: callArgs,
21
+ callContext: callContext,
22
+ children: [],
23
+ isRecursive: isRecursiveCall,
24
+ recursiveTargetNodeId: recursiveTargetNodeId
25
+ };
26
+ if (isRecursiveCall) {
27
+ return node;
28
+ }
29
+ context.callStack.push(functionAstId);
30
+ context.activeNodes.set(functionAstId, node.nodeId);
31
+ // For modifiers, process argument calls first (before modifier body)
32
+ if (currentFunc instanceof solc_typed_ast_1.ModifierDefinition && fnCall instanceof solc_typed_ast_1.ModifierInvocation) {
33
+ for (const arg of fnCall.vArguments) {
34
+ (0, expression_analyzer_1.collectCallsFromExpression)(arg, node.children, fnCall, undefined, (func, args, call, ctx) => buildCallTree(func, args, call, ctx, context), context.contract);
35
+ }
36
+ }
37
+ // For constructors, process argument calls first (before constructor body)
38
+ if (currentFunc instanceof solc_typed_ast_1.FunctionDefinition && currentFunc.isConstructor && fnCall instanceof solc_typed_ast_1.ModifierInvocation) {
39
+ for (const arg of fnCall.vArguments) {
40
+ (0, expression_analyzer_1.collectCallsFromExpression)(arg, node.children, fnCall, undefined, (func, args, call, ctx) => buildCallTree(func, args, call, ctx, context), context.contract);
41
+ }
42
+ }
43
+ // Process function body first - only direct statements
44
+ if (currentFunc.vBody) {
45
+ // For modifiers, create the context for parameter resolution
46
+ const modifierContext = currentFunc instanceof solc_typed_ast_1.ModifierDefinition ?
47
+ { modifier: currentFunc, resolvedArgs: callArgs } : undefined;
48
+ for (const stmt of currentFunc.vBody.vStatements) {
49
+ (0, expression_analyzer_1.collectCallsFromExpression)(stmt, node.children, stmt, modifierContext, (func, args, call, ctx) => buildCallTree(func, args, call, ctx, context), context.contract);
50
+ }
51
+ }
52
+ // Helper function to resolve modifier arguments based on function parameters
53
+ function resolveModifierArguments(func, modifierArgs, functionCallArgs) {
54
+ return modifierArgs.map(arg => {
55
+ // If the argument is an Identifier that matches a function parameter, replace it with the actual argument
56
+ if (arg instanceof solc_typed_ast_1.Identifier) {
57
+ const paramIndex = func.vParameters.vParameters.findIndex(param => param.name === arg.name);
58
+ if (paramIndex !== -1 && paramIndex < functionCallArgs.length) {
59
+ return functionCallArgs[paramIndex];
60
+ }
61
+ }
62
+ return arg;
63
+ });
64
+ }
65
+ // Process modifiers last for ALL functions (at the end of the frame in reverse order)
66
+ if (currentFunc instanceof solc_typed_ast_1.FunctionDefinition && currentFunc.vModifiers.length > 0) {
67
+ // Reverse the order of modifiers and add them at the end
68
+ const reversedModifiers = [...currentFunc.vModifiers].reverse();
69
+ for (const modifierInvocation of reversedModifiers) {
70
+ if (modifierInvocation.vModifierName.vReferencedDeclaration instanceof solc_typed_ast_1.ModifierDefinition) {
71
+ const calledModifier = modifierInvocation.vModifierName.vReferencedDeclaration;
72
+ // Resolve modifier arguments based on the actual function call arguments
73
+ const resolvedArgs = resolveModifierArguments(currentFunc, modifierInvocation.vArguments, callArgs);
74
+ node.children.push(buildCallTree(calledModifier, resolvedArgs, modifierInvocation, undefined, context));
75
+ }
76
+ else if (modifierInvocation.vModifierName.vReferencedDeclaration instanceof solc_typed_ast_1.ContractDefinition) {
77
+ // Handle constructor calls like ERC20("ETH", 18)
78
+ const baseContract = modifierInvocation.vModifierName.vReferencedDeclaration;
79
+ // Find the constructor in the base contract
80
+ const constructor = baseContract.vFunctions.find(f => f.isConstructor);
81
+ if (constructor) {
82
+ // Resolve constructor arguments as well
83
+ const resolvedArgs = resolveModifierArguments(currentFunc, modifierInvocation.vArguments, callArgs);
84
+ node.children.push(buildCallTree(constructor, resolvedArgs, modifierInvocation, undefined, context));
85
+ }
86
+ }
87
+ }
88
+ }
89
+ context.callStack.pop();
90
+ context.activeNodes.delete(functionAstId);
91
+ return node;
92
+ }
@@ -0,0 +1,14 @@
1
+ import { ASTNode, ContractDefinition, Expression, FunctionCall, FunctionDefinition, ModifierDefinition, Statement } from 'solc-typed-ast';
2
+ import { CallTree } from '../types';
3
+ export interface ModifierContext {
4
+ modifier: ModifierDefinition;
5
+ resolvedArgs: Expression[];
6
+ }
7
+ /**
8
+ * Helper to resolve modifier parameters in expressions
9
+ */
10
+ export declare function resolveModifierParams(expr: Expression, modifierContext?: ModifierContext): Expression;
11
+ /**
12
+ * Helper to collect direct function/library calls from an expression
13
+ */
14
+ export declare function collectCallsFromExpression(expr: ASTNode, childrenArr: CallTree[], context: Statement, modifierContext?: ModifierContext, buildCallTree?: (currentFunc: FunctionDefinition | ModifierDefinition, callArgs: Expression[], fnCall?: FunctionCall, callContext?: Statement) => CallTree, contract?: ContractDefinition): void;
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveModifierParams = resolveModifierParams;
4
+ exports.collectCallsFromExpression = collectCallsFromExpression;
5
+ const solc_typed_ast_1 = require("solc-typed-ast");
6
+ const utils_1 = require("../utils");
7
+ const utils_2 = require("../utils");
8
+ const utils_3 = require("../utils");
9
+ /**
10
+ * Helper to resolve modifier parameters in expressions
11
+ */
12
+ function resolveModifierParams(expr, modifierContext) {
13
+ if (!modifierContext)
14
+ return expr;
15
+ // If it's an identifier that matches a modifier parameter, replace it
16
+ if (expr instanceof solc_typed_ast_1.Identifier) {
17
+ const paramIndex = modifierContext.modifier.vParameters.vParameters.findIndex(param => param.name === expr.name);
18
+ if (paramIndex !== -1 && paramIndex < modifierContext.resolvedArgs.length) {
19
+ return modifierContext.resolvedArgs[paramIndex];
20
+ }
21
+ }
22
+ return expr;
23
+ }
24
+ /**
25
+ * Helper to collect direct function/library calls from an expression
26
+ */
27
+ function collectCallsFromExpression(expr, childrenArr, context, modifierContext, buildCallTree, contract) {
28
+ if (!expr || !buildCallTree || !contract)
29
+ return;
30
+ // Find all function calls in this expression (include the root node itself if it is a call)
31
+ const rootCalls = [];
32
+ if (expr instanceof solc_typed_ast_1.FunctionCall) {
33
+ rootCalls.push(expr);
34
+ }
35
+ const allCalls = [...rootCalls, ...expr.getChildrenByType(solc_typed_ast_1.FunctionCall, true)].filter(c => !(0, utils_1.highLevelCall)(c) &&
36
+ !(0, utils_1.highLevelCallWithOptions)(c) &&
37
+ c.kind === solc_typed_ast_1.FunctionCallKind.FunctionCall &&
38
+ !!c.vReferencedDeclaration &&
39
+ (c.vReferencedDeclaration instanceof solc_typed_ast_1.FunctionDefinition || c.vReferencedDeclaration instanceof solc_typed_ast_1.ModifierDefinition));
40
+ // Deduplicate in case the root call also appears in children traversal
41
+ const uniqueCalls = Array.from(new Set(allCalls));
42
+ // Implement proper Solidity evaluation order
43
+ const evaluationOrder = [];
44
+ // Recursive function to traverse and collect calls in correct evaluation order
45
+ function collectInEvaluationOrder(node, visited = new Set()) {
46
+ if (!node)
47
+ return;
48
+ // If this is a function call we care about, process it
49
+ if (node instanceof solc_typed_ast_1.FunctionCall &&
50
+ !(0, utils_1.highLevelCall)(node) &&
51
+ !(0, utils_1.highLevelCallWithOptions)(node) &&
52
+ node.kind === solc_typed_ast_1.FunctionCallKind.FunctionCall &&
53
+ !!node.vReferencedDeclaration &&
54
+ (node.vReferencedDeclaration instanceof solc_typed_ast_1.FunctionDefinition || node.vReferencedDeclaration instanceof solc_typed_ast_1.ModifierDefinition) &&
55
+ uniqueCalls.includes(node) &&
56
+ !visited.has(node)) {
57
+ // First, evaluate all arguments (which may contain nested calls)
58
+ for (const arg of node.vArguments) {
59
+ collectInEvaluationOrder(arg, visited);
60
+ }
61
+ // Then add this call to the evaluation order
62
+ evaluationOrder.push(node);
63
+ visited.add(node);
64
+ return;
65
+ }
66
+ // For binary operations, evaluate right operand first, then left operand (Solidity stack-based evaluation)
67
+ if (node.type === 'BinaryOperation') {
68
+ const binaryOp = node;
69
+ // Access left and right operands through children array
70
+ const left = binaryOp.children && binaryOp.children[0];
71
+ const right = binaryOp.children && binaryOp.children[1];
72
+ if (right)
73
+ collectInEvaluationOrder(right, visited);
74
+ if (left)
75
+ collectInEvaluationOrder(left, visited);
76
+ return;
77
+ }
78
+ // For other nodes, recursively process children in order
79
+ const children = node.children || [];
80
+ for (const child of children) {
81
+ collectInEvaluationOrder(child, visited);
82
+ }
83
+ }
84
+ // Start the evaluation order collection from the root expression
85
+ collectInEvaluationOrder(expr);
86
+ // Use the collected evaluation order
87
+ const sortedCalls = evaluationOrder.length > 0 ? evaluationOrder : uniqueCalls;
88
+ // Filter to get only direct calls while preserving evaluation order
89
+ const directCalls = sortedCalls.filter(call => {
90
+ // A call is direct if no other call in this expression contains it
91
+ return !uniqueCalls.some(otherCall => otherCall !== call &&
92
+ otherCall.getChildrenByType(solc_typed_ast_1.FunctionCall, true).includes(call));
93
+ });
94
+ for (const call of directCalls) {
95
+ let callNode = null;
96
+ // Library call via MemberAccess
97
+ if (call.vExpression instanceof solc_typed_ast_1.MemberAccess) {
98
+ const memberAccess = call.vExpression;
99
+ if (memberAccess.vReferencedDeclaration instanceof solc_typed_ast_1.FunctionDefinition) {
100
+ const libraryFunc = memberAccess.vReferencedDeclaration;
101
+ if (memberAccess.vExpression instanceof solc_typed_ast_1.Identifier) {
102
+ // Handle 'super' receiver first (special case)
103
+ if (memberAccess.vExpression.name === 'super') {
104
+ const currentContract = (0, utils_3.getEnclosingContract)(call);
105
+ if (currentContract) {
106
+ const targetFunc = (0, utils_3.resolveSuper)(contract, currentContract, (0, utils_2.getSignature)(libraryFunc));
107
+ if (targetFunc && targetFunc.implemented) {
108
+ const superArgs = call.vArguments.map((arg) => resolveModifierParams(arg, modifierContext));
109
+ callNode = buildCallTree(targetFunc, superArgs, call, context);
110
+ }
111
+ }
112
+ // Do NOT process further as library call
113
+ }
114
+ else {
115
+ const receiverDeclaration = memberAccess.vExpression.vReferencedDeclaration;
116
+ if (receiverDeclaration && receiverDeclaration instanceof solc_typed_ast_1.ContractDefinition) {
117
+ // Direct library call: HelperLibrary.increase(10)
118
+ if (libraryFunc.implemented) {
119
+ const resolvedLibraryArgs = call.vArguments.map((arg) => resolveModifierParams(arg, modifierContext));
120
+ callNode = buildCallTree(libraryFunc, resolvedLibraryArgs, call, context);
121
+ }
122
+ }
123
+ else {
124
+ // "Using for" call: result.increase()
125
+ if (libraryFunc.implemented) {
126
+ const resolvedReceiver = resolveModifierParams(memberAccess.vExpression, modifierContext);
127
+ const resolvedArgs = call.vArguments.map((arg) => resolveModifierParams(arg, modifierContext));
128
+ const libraryArgs = [resolvedReceiver, ...resolvedArgs];
129
+ callNode = buildCallTree(libraryFunc, libraryArgs, call, context);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ else {
135
+ // Complex receiver - collect receiver calls to add as children of this call
136
+ const receiverCalls = [];
137
+ collectCallsFromExpression(memberAccess.vExpression, receiverCalls, context, modifierContext, buildCallTree, contract);
138
+ // Then add the library call with receiver calls as children
139
+ if (libraryFunc.implemented) {
140
+ const resolvedReceiver = resolveModifierParams(memberAccess.vExpression, modifierContext);
141
+ const resolvedArgs = call.vArguments.map((arg) => resolveModifierParams(arg, modifierContext));
142
+ const libraryArgs = [resolvedReceiver, ...resolvedArgs];
143
+ callNode = buildCallTree(libraryFunc, libraryArgs, call, context);
144
+ // Add receiver calls as children (before body calls)
145
+ if (callNode) {
146
+ callNode.children = [...receiverCalls, ...callNode.children];
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ else {
153
+ // Direct function call (could be internal/virtual and overridden)
154
+ const originalFunc = call.vReferencedDeclaration;
155
+ const resolved = (0, utils_3.resolveOverride)(contract, (0, utils_2.getSignature)(originalFunc)) || originalFunc;
156
+ if (resolved.implemented) {
157
+ // Resolve modifier parameters in call arguments
158
+ const resolvedCallArgs = call.vArguments.map((arg) => resolveModifierParams(arg, modifierContext));
159
+ callNode = buildCallTree(resolved, resolvedCallArgs, call, context);
160
+ }
161
+ }
162
+ // If we created a call node, process its arguments for nested calls
163
+ if (callNode) {
164
+ // First, recursively process arguments to find nested calls (these should come first)
165
+ const argumentCalls = [];
166
+ for (const arg of call.vArguments) {
167
+ collectCallsFromExpression(arg, argumentCalls, context, modifierContext, buildCallTree, contract);
168
+ }
169
+ // Then process function body calls (from buildCallTree)
170
+ const bodyCalls = callNode.children;
171
+ // Combine: argument calls first, then body calls
172
+ callNode.children = [...argumentCalls, ...bodyCalls];
173
+ childrenArr.push(callNode);
174
+ }
175
+ }
176
+ }
@@ -0,0 +1 @@
1
+ export declare const runCoverage: (foundryRoot: string, foundryConfigPath: string, cryticName: string, suiteNameSnake?: string) => Promise<void>;
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runCoverage = void 0;
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs/promises"));
39
+ const path = __importStar(require("path"));
40
+ const solc_typed_ast_1 = require("solc-typed-ast");
41
+ const processor_1 = require("./processor");
42
+ const types_1 = require("./types");
43
+ const utils_1 = require("./utils");
44
+ const isSetupName = (name) => {
45
+ if (!name)
46
+ return false;
47
+ return name.toLowerCase() === 'setup';
48
+ };
49
+ const stripDataLocation = (raw) => {
50
+ if (!raw)
51
+ return '';
52
+ const noLocation = raw
53
+ .replace(/\s+(storage|memory|calldata)(\s+pointer)?/g, '')
54
+ .replace(/\s+pointer/g, '');
55
+ const noPrefix = noLocation
56
+ .replace(/^struct\s+/, '')
57
+ .replace(/^enum\s+/, '')
58
+ .replace(/^contract\s+/, '')
59
+ .trim();
60
+ return noPrefix;
61
+ };
62
+ const signatureFromFnDef = (fnDef) => {
63
+ const params = fnDef.vParameters.vParameters
64
+ .map((p) => { var _a; return stripDataLocation((_a = p.typeString) !== null && _a !== void 0 ? _a : ''); })
65
+ .join(',');
66
+ const name = fnDef.name
67
+ || (fnDef.kind === solc_typed_ast_1.FunctionKind.Constructor
68
+ ? 'constructor'
69
+ : fnDef.kind === solc_typed_ast_1.FunctionKind.Fallback
70
+ ? 'fallback'
71
+ : fnDef.kind === solc_typed_ast_1.FunctionKind.Receive
72
+ ? 'receive'
73
+ : '');
74
+ return `${name}(${params})`;
75
+ };
76
+ const runCmd = (cmd, cwd) => {
77
+ return new Promise((resolve, reject) => {
78
+ (0, child_process_1.exec)(cmd, { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (err, _stdout, stderr) => {
79
+ if (err) {
80
+ reject(new Error(stderr || err.message));
81
+ }
82
+ else {
83
+ resolve();
84
+ }
85
+ });
86
+ });
87
+ };
88
+ const loadLatestSourceUnits = async (foundryRoot) => {
89
+ var _a;
90
+ const outDir = path.join(foundryRoot, '.recon', 'out');
91
+ const buildInfoDir = path.join(outDir, 'build-info');
92
+ let files = [];
93
+ try {
94
+ const entries = await fs.readdir(buildInfoDir);
95
+ const jsonFiles = entries.filter((f) => f.endsWith('.json'));
96
+ files = await Promise.all(jsonFiles.map(async (f) => ({
97
+ name: f,
98
+ path: path.join(buildInfoDir, f),
99
+ mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime,
100
+ })));
101
+ }
102
+ catch (e) {
103
+ throw new Error(`No build-info directory found at ${buildInfoDir}: ${e}`);
104
+ }
105
+ if (files.length === 0) {
106
+ throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
107
+ }
108
+ const latestFile = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path;
109
+ const fileContent = await fs.readFile(latestFile, 'utf-8');
110
+ const buildInfo = JSON.parse(fileContent);
111
+ const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
112
+ if (!buildOutput) {
113
+ throw new Error(`Build-info file ${latestFile} is missing output data.`);
114
+ }
115
+ const filteredAstData = { ...buildOutput };
116
+ if (filteredAstData.sources) {
117
+ const validSources = {};
118
+ for (const [key, content] of Object.entries(filteredAstData.sources)) {
119
+ const ast = content.ast || content.legacyAST || content.AST;
120
+ if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
121
+ validSources[key] = content;
122
+ }
123
+ }
124
+ filteredAstData.sources = validSources;
125
+ }
126
+ const reader = new solc_typed_ast_1.ASTReader();
127
+ return reader.read(filteredAstData);
128
+ };
129
+ const shouldIncludeFunction = (fnDef) => {
130
+ if (!fnDef.implemented)
131
+ return false;
132
+ if (isSetupName(fnDef.name))
133
+ return false;
134
+ if (fnDef.kind !== solc_typed_ast_1.FunctionKind.Function)
135
+ return false;
136
+ if (fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.External &&
137
+ fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.Public) {
138
+ return false;
139
+ }
140
+ if (fnDef.stateMutability === solc_typed_ast_1.FunctionStateMutability.View ||
141
+ fnDef.stateMutability === solc_typed_ast_1.FunctionStateMutability.Pure) {
142
+ return false;
143
+ }
144
+ return true;
145
+ };
146
+ const addFunction = (map, contractName, fnDef) => {
147
+ var _a;
148
+ const sig = signatureFromFnDef(fnDef);
149
+ if (!sig)
150
+ return;
151
+ const existing = (_a = map.get(contractName)) !== null && _a !== void 0 ? _a : new Set();
152
+ existing.add(sig);
153
+ map.set(contractName, existing);
154
+ };
155
+ const collectContractFunctions = (sourceUnits, cryticName) => {
156
+ var _a;
157
+ const functionsByContract = new Map();
158
+ const contracts = [];
159
+ for (const unit of sourceUnits) {
160
+ for (const contract of unit.getChildrenByType(solc_typed_ast_1.ContractDefinition)) {
161
+ if (contract.name === cryticName) {
162
+ contracts.push(contract);
163
+ }
164
+ }
165
+ }
166
+ if (contracts.length === 0) {
167
+ throw new Error(`Contract ${cryticName} not found in build output.`);
168
+ }
169
+ const contract = contracts[0];
170
+ const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
171
+ for (const fnDef of allFunctions) {
172
+ if (!shouldIncludeFunction(fnDef)) {
173
+ continue;
174
+ }
175
+ addFunction(functionsByContract, contract.name, fnDef);
176
+ for (const call of fnDef.getChildrenByType(solc_typed_ast_1.FunctionCall)) {
177
+ const callType = (0, utils_1.getCallType)(call);
178
+ if (callType === types_1.CallType.Internal) {
179
+ continue;
180
+ }
181
+ const ref = (_a = call.vExpression.vReferencedDeclaration) !== null && _a !== void 0 ? _a : call.vReferencedDeclaration;
182
+ if (!(ref instanceof solc_typed_ast_1.FunctionDefinition)) {
183
+ continue;
184
+ }
185
+ const targetContract = ref.getClosestParentByType(solc_typed_ast_1.ContractDefinition);
186
+ if (!targetContract) {
187
+ continue;
188
+ }
189
+ if (ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.View ||
190
+ ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.Pure) {
191
+ continue;
192
+ }
193
+ if (ref.kind === solc_typed_ast_1.FunctionKind.Constructor ||
194
+ ref.kind === solc_typed_ast_1.FunctionKind.Fallback ||
195
+ ref.kind === solc_typed_ast_1.FunctionKind.Receive) {
196
+ continue;
197
+ }
198
+ addFunction(functionsByContract, targetContract.name, ref);
199
+ }
200
+ }
201
+ return functionsByContract;
202
+ };
203
+ const runCoverage = async (foundryRoot, foundryConfigPath, cryticName, suiteNameSnake) => {
204
+ const outDir = path.join(foundryRoot, '.recon', 'out');
205
+ const buildCmd = `forge build --contracts ${cryticName} --build-info --out .recon/out`.replace(/\s+/g, ' ').trim();
206
+ await runCmd(buildCmd, foundryRoot);
207
+ const sourceUnits = await loadLatestSourceUnits(foundryRoot);
208
+ if (!sourceUnits || sourceUnits.length === 0) {
209
+ throw new Error('No source units were produced from the Crytic build; cannot generate coverage.');
210
+ }
211
+ const contractFunctions = collectContractFunctions(sourceUnits, cryticName);
212
+ if (contractFunctions.size === 0) {
213
+ throw new Error('No eligible functions found to include in coverage.');
214
+ }
215
+ const coverage = await (0, processor_1.buildCoverageMap)(sourceUnits, foundryRoot, contractFunctions);
216
+ const coverageName = suiteNameSnake ? `recon-${suiteNameSnake}-coverage.json` : 'recon-coverage.json';
217
+ const coveragePath = path.join(foundryRoot, coverageName);
218
+ await fs.writeFile(coveragePath, JSON.stringify(coverage, null, 2));
219
+ console.log(`[recon-generate] Wrote coverage file to ${coveragePath}`);
220
+ };
221
+ exports.runCoverage = runCoverage;
package/dist/generator.js CHANGED
@@ -111,14 +111,14 @@ class ReconGenerator {
111
111
  if (await (0, utils_1.fileExists)(chimeraPath)) {
112
112
  return;
113
113
  }
114
- await this.runCmd('forge install Recon-Fuzz/chimera --no-git', this.foundryRoot);
114
+ await this.runCmd('forge install Recon-Fuzz/chimera', this.foundryRoot);
115
115
  }
116
116
  async installSetupHelpers() {
117
117
  const setupPath = path.join(this.foundryRoot, 'lib', 'setup-helpers');
118
118
  if (await (0, utils_1.fileExists)(setupPath)) {
119
119
  return;
120
120
  }
121
- await this.runCmd('forge install Recon-Fuzz/setup-helpers --no-git', this.foundryRoot);
121
+ await this.runCmd('forge install Recon-Fuzz/setup-helpers', this.foundryRoot);
122
122
  }
123
123
  async updateRemappings() {
124
124
  const remappingsPath = path.join(this.foundryRoot, 'remappings.txt');
package/dist/index.js CHANGED
@@ -38,6 +38,8 @@ const commander_1 = require("commander");
38
38
  const path = __importStar(require("path"));
39
39
  const case_1 = require("case");
40
40
  const generator_1 = require("./generator");
41
+ const coverage_1 = require("./coverage");
42
+ const pathsGenerator_1 = require("./pathsGenerator");
41
43
  const utils_1 = require("./utils");
42
44
  const link_1 = require("./link");
43
45
  function parseFilter(input) {
@@ -122,6 +124,38 @@ async function main() {
122
124
  .option('--force', 'Replace existing generated suite output (under --output). Does not rebuild .recon/out.')
123
125
  .option('--force-build', 'Delete .recon/out to force a fresh forge build before generating')
124
126
  .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)');
127
+ program
128
+ .command('coverage')
129
+ .description('Generate recon-coverage.json from a Crytic tester contract without scaffolding tests')
130
+ .option('--crytic-name <name>', 'Name of the Crytic tester contract to compile', 'CryticTester')
131
+ .option('--name <suite>', 'Suite name; affects coverage filename (recon-<name>-coverage.json)')
132
+ .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
133
+ .action(async (opts, cmd) => {
134
+ var _a;
135
+ const workspaceRoot = process.cwd();
136
+ const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
137
+ const foundryRoot = path.dirname(foundryConfig);
138
+ const parentOpts = ((_a = cmd.parent) === null || _a === void 0 ? void 0 : _a.opts()) || {};
139
+ const suiteRaw = opts.name || parentOpts.name ? String(opts.name || parentOpts.name).trim() : '';
140
+ const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
141
+ await (0, coverage_1.runCoverage)(foundryRoot, foundryConfig, opts.cryticName || 'CryticTester', suiteSnake);
142
+ });
143
+ program
144
+ .command('paths')
145
+ .description('Generate recon-paths.json from a Crytic tester contract without scaffolding tests')
146
+ .option('--crytic-name <name>', 'Name of the Crytic tester contract to compile', 'CryticTester')
147
+ .option('--name <suite>', 'Suite name; affects paths filename (recon-<name>-paths.json)')
148
+ .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
149
+ .action(async (opts, cmd) => {
150
+ var _a;
151
+ const workspaceRoot = process.cwd();
152
+ const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
153
+ const foundryRoot = path.dirname(foundryConfig);
154
+ const parentOpts = ((_a = cmd.parent) === null || _a === void 0 ? void 0 : _a.opts()) || {};
155
+ const suiteRaw = opts.name || parentOpts.name ? String(opts.name || parentOpts.name).trim() : '';
156
+ const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
157
+ await (0, pathsGenerator_1.runPaths)(foundryRoot, opts.cryticName || 'CryticTester', suiteSnake);
158
+ });
125
159
  program
126
160
  .command('link')
127
161
  .description('Link library addresses into echidna/medusa configs via crytic-compile')
@@ -129,11 +163,13 @@ async function main() {
129
163
  .option('--medusa-config <path>', 'Path to medusa json (defaults based on --name)')
130
164
  .option('--name <suite>', 'Suite name to pick config defaults (echidna-<name>.yaml / medusa-<name>.json)')
131
165
  .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
132
- .action(async (opts) => {
166
+ .action(async (opts, cmd) => {
167
+ var _a;
133
168
  const workspaceRoot = process.cwd();
134
169
  const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
135
170
  const foundryRoot = path.dirname(foundryConfig);
136
- const suiteRaw = opts.name ? String(opts.name).trim() : '';
171
+ const parentOpts = ((_a = cmd.parent) === null || _a === void 0 ? void 0 : _a.opts()) || {};
172
+ const suiteRaw = opts.name || parentOpts.name ? String(opts.name || parentOpts.name).trim() : '';
137
173
  const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
138
174
  const suffix = suiteSnake ? `-${suiteSnake}` : '';
139
175
  const echidnaDefault = `echidna${suffix}.yaml`;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Path Generator
3
+ *
4
+ * Generates minimal paths needed for 100% branch coverage.
5
+ * Output is optimized for LLM consumption.
6
+ */
7
+ /** Simple output: function name -> array of path condition strings */
8
+ export type PathOutput = Record<string, string[]>;
9
+ export declare const runPaths: (foundryRoot: string, cryticName: string, suiteNameSnake?: string) => Promise<void>;