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