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 +3 -0
- package/dist/analyzer/call-tree-builder.d.ts +14 -0
- package/dist/analyzer/call-tree-builder.js +92 -0
- package/dist/analyzer/expression-analyzer.d.ts +14 -0
- package/dist/analyzer/expression-analyzer.js +176 -0
- package/dist/coverage.d.ts +1 -0
- package/dist/coverage.js +221 -0
- package/dist/generator.js +2 -2
- package/dist/index.js +38 -2
- package/dist/pathsGenerator.d.ts +9 -0
- package/dist/pathsGenerator.js +627 -0
- package/dist/processor.js +18 -0
- package/dist/types.d.ts +11 -1
- package/dist/utils.d.ts +11 -1
- package/dist/utils.js +108 -5
- package/package.json +1 -1
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>;
|
package/dist/coverage.js
ADDED
|
@@ -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
|
|
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
|
|
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
|
|
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>;
|