recon-generate 0.0.7 → 0.0.9

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,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
+ }
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ const path = __importStar(require("path"));
39
39
  const case_1 = require("case");
40
40
  const generator_1 = require("./generator");
41
41
  const coverage_1 = require("./coverage");
42
+ const pathsGenerator_1 = require("./pathsGenerator");
42
43
  const utils_1 = require("./utils");
43
44
  const link_1 = require("./link");
44
45
  function parseFilter(input) {
@@ -129,14 +130,32 @@ async function main() {
129
130
  .option('--crytic-name <name>', 'Name of the Crytic tester contract to compile', 'CryticTester')
130
131
  .option('--name <suite>', 'Suite name; affects coverage filename (recon-<name>-coverage.json)')
131
132
  .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
132
- .action(async (opts) => {
133
+ .action(async (opts, cmd) => {
134
+ var _a;
133
135
  const workspaceRoot = process.cwd();
134
136
  const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
135
137
  const foundryRoot = path.dirname(foundryConfig);
136
- const suiteRaw = opts.name ? String(opts.name).trim() : '';
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() : '';
137
140
  const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
138
141
  await (0, coverage_1.runCoverage)(foundryRoot, foundryConfig, opts.cryticName || 'CryticTester', suiteSnake);
139
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
+ });
140
159
  program
141
160
  .command('link')
142
161
  .description('Link library addresses into echidna/medusa configs via crytic-compile')
@@ -144,11 +163,14 @@ async function main() {
144
163
  .option('--medusa-config <path>', 'Path to medusa json (defaults based on --name)')
145
164
  .option('--name <suite>', 'Suite name to pick config defaults (echidna-<name>.yaml / medusa-<name>.json)')
146
165
  .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
147
- .action(async (opts) => {
166
+ .option('--verbose', 'Enable verbose output for debugging')
167
+ .action(async (opts, cmd) => {
168
+ var _a;
148
169
  const workspaceRoot = process.cwd();
149
170
  const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
150
171
  const foundryRoot = path.dirname(foundryConfig);
151
- const suiteRaw = opts.name ? String(opts.name).trim() : '';
172
+ const parentOpts = ((_a = cmd.parent) === null || _a === void 0 ? void 0 : _a.opts()) || {};
173
+ const suiteRaw = opts.name || parentOpts.name ? String(opts.name || parentOpts.name).trim() : '';
152
174
  const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
153
175
  const suffix = suiteSnake ? `-${suiteSnake}` : '';
154
176
  const echidnaDefault = `echidna${suffix}.yaml`;
@@ -161,7 +183,7 @@ async function main() {
161
183
  const medusaConfigPath = path.isAbsolute(medusaConfigOpt)
162
184
  ? medusaConfigOpt
163
185
  : path.join(foundryRoot, medusaConfigOpt);
164
- await (0, link_1.runLink)(foundryRoot, echidnaConfigPath, medusaConfigPath);
186
+ await (0, link_1.runLink)(foundryRoot, echidnaConfigPath, medusaConfigPath, !!opts.verbose);
165
187
  });
166
188
  program
167
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
  }
@@ -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>;
@@ -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/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
+ };
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ASTNode, ContractDefinition, Assignment, FunctionCall, FunctionDefinition, VariableDeclaration } from 'solc-typed-ast';
1
+ import { ASTNode, ContractDefinition, Assignment, FunctionCall, FunctionDefinition, VariableDeclaration, EventDefinition, ErrorDefinition } from 'solc-typed-ast';
2
2
  import { CallType } from './types';
3
3
  export declare function fileExists(p: string): Promise<boolean>;
4
4
  export declare function getFoundryConfigPath(workspaceRoot: string, override?: string): string;
@@ -29,3 +29,13 @@ export declare function toSource(node: ASTNode, version?: string): string;
29
29
  export declare function getCallType(fnCall: FunctionCall): CallType;
30
30
  export declare const getFunctionName: (fnDef: FunctionDefinition) => string;
31
31
  export declare const signatureEquals: (a: FunctionDefinition, b: FunctionDefinition) => boolean;
32
+ export declare function getSignature(definition: FunctionDefinition | EventDefinition | ErrorDefinition): string;
33
+ export declare function getEnclosingContract(node: ASTNode): ContractDefinition | undefined;
34
+ export declare function resolveOverride(root: ContractDefinition, signature: string): FunctionDefinition | undefined;
35
+ export declare function idToContract(root: ContractDefinition, id: number): ContractDefinition | undefined;
36
+ export declare function forwardLinearization(root: ContractDefinition): ContractDefinition[];
37
+ /**
38
+ * Resolve the `super` call according to Solidity's C3 linearisation of the root
39
+ * contract. `current` is the contract whose code contains the `super` call.
40
+ */
41
+ export declare function resolveSuper(root: ContractDefinition, current: ContractDefinition, signature: string): FunctionDefinition | undefined;
package/dist/utils.js CHANGED
@@ -58,6 +58,12 @@ exports.getDeepRef = getDeepRef;
58
58
  exports.getDefinitions = getDefinitions;
59
59
  exports.toSource = toSource;
60
60
  exports.getCallType = getCallType;
61
+ exports.getSignature = getSignature;
62
+ exports.getEnclosingContract = getEnclosingContract;
63
+ exports.resolveOverride = resolveOverride;
64
+ exports.idToContract = idToContract;
65
+ exports.forwardLinearization = forwardLinearization;
66
+ exports.resolveSuper = resolveSuper;
61
67
  const fs = __importStar(require("fs/promises"));
62
68
  const path = __importStar(require("path"));
63
69
  const solc_typed_ast_1 = require("solc-typed-ast");
@@ -349,3 +355,84 @@ const signatureEquals = (a, b) => {
349
355
  return a.stateMutability === b.stateMutability;
350
356
  };
351
357
  exports.signatureEquals = signatureEquals;
358
+ function getSignature(definition) {
359
+ let name;
360
+ if (definition instanceof solc_typed_ast_1.FunctionDefinition) {
361
+ if (definition.kind === solc_typed_ast_1.FunctionKind.Constructor) {
362
+ name = 'constructor';
363
+ }
364
+ else if (definition.kind === solc_typed_ast_1.FunctionKind.Fallback) {
365
+ name = 'fallback';
366
+ }
367
+ else if (definition.kind === solc_typed_ast_1.FunctionKind.Receive) {
368
+ name = 'receive';
369
+ }
370
+ else {
371
+ name = definition.name;
372
+ }
373
+ }
374
+ else {
375
+ name = definition.name;
376
+ }
377
+ const paramTypes = definition.vParameters.vParameters.map(param => param.typeString).join(',');
378
+ return `${name}(${paramTypes})`;
379
+ }
380
+ function getEnclosingContract(node) {
381
+ let cur = node;
382
+ while (cur) {
383
+ if (cur instanceof solc_typed_ast_1.FunctionDefinition && cur.vScope instanceof solc_typed_ast_1.ContractDefinition) {
384
+ return cur.vScope;
385
+ }
386
+ cur = cur.parent;
387
+ }
388
+ return undefined;
389
+ }
390
+ function resolveOverride(root, signature) {
391
+ for (const ctr of forwardLinearization(root)) {
392
+ const match = ctr.vFunctions.find(fn => getSignature(fn) === signature);
393
+ if (match)
394
+ return match;
395
+ }
396
+ return undefined;
397
+ }
398
+ function idToContract(root, id) {
399
+ try {
400
+ return root.requiredContext.locate(id);
401
+ }
402
+ catch {
403
+ return undefined;
404
+ }
405
+ }
406
+ // Build forward (source-order) linearized list: root, then direct bases left->right, then their bases recursively without duplicates.
407
+ function forwardLinearization(root) {
408
+ // Solidity compiler provides the C3 linearized order in `linearizedBaseContracts`.
409
+ // We rely on that order directly; it already starts with `root` and follows the
410
+ // resolution order used for `super`.
411
+ const ids = root.linearizedBaseContracts;
412
+ const contracts = [];
413
+ for (const id of ids) {
414
+ const ctr = idToContract(root, id);
415
+ if (ctr)
416
+ contracts.push(ctr);
417
+ }
418
+ return contracts;
419
+ }
420
+ /**
421
+ * Resolve the `super` call according to Solidity's C3 linearisation of the root
422
+ * contract. `current` is the contract whose code contains the `super` call.
423
+ */
424
+ function resolveSuper(root, current, signature) {
425
+ // Linearised list of contract IDs for `root` (e.g. [D, B, C, A])
426
+ const linearContracts = forwardLinearization(root);
427
+ // Find index of the current contract
428
+ const idx = linearContracts.findIndex(c => c.id === current.id);
429
+ if (idx === -1)
430
+ return undefined;
431
+ // Search forward for first contract implementing the function
432
+ for (let i = idx + 1; i < linearContracts.length; i++) {
433
+ const cand = linearContracts[i].vFunctions.find(fn => getSignature(fn) === signature);
434
+ if (cand)
435
+ return cand;
436
+ }
437
+ return undefined;
438
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recon-generate",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {