recon-generate 0.0.15 → 0.0.16
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/dist/index.js +18 -0
- package/dist/info.d.ts +39 -0
- package/dist/info.js +984 -0
- package/dist/z3Solver.d.ts +11 -0
- package/dist/z3Solver.js +425 -0
- package/package.json +3 -2
package/dist/info.js
ADDED
|
@@ -0,0 +1,984 @@
|
|
|
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.runInfo = 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 utils_1 = require("./utils");
|
|
42
|
+
const z3Solver_1 = require("./z3Solver");
|
|
43
|
+
const call_tree_builder_1 = require("./analyzer/call-tree-builder");
|
|
44
|
+
const runCmd = (cmd, cwd) => {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
(0, child_process_1.exec)(cmd, { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (err, _stdout, stderr) => {
|
|
47
|
+
if (err) {
|
|
48
|
+
reject(new Error(stderr || err.message));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
resolve();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
const loadLatestBuildInfo = async (foundryRoot) => {
|
|
57
|
+
var _a;
|
|
58
|
+
const outDir = path.join(foundryRoot, 'out');
|
|
59
|
+
const buildInfoDir = path.join(outDir, 'build-info');
|
|
60
|
+
let files = [];
|
|
61
|
+
try {
|
|
62
|
+
const entries = await fs.readdir(buildInfoDir);
|
|
63
|
+
const jsonFiles = entries.filter((f) => f.endsWith('.json'));
|
|
64
|
+
files = await Promise.all(jsonFiles.map(async (f) => ({
|
|
65
|
+
name: f,
|
|
66
|
+
path: path.join(buildInfoDir, f),
|
|
67
|
+
mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime,
|
|
68
|
+
})));
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
throw new Error(`No build-info directory found at ${buildInfoDir}: ${e}`);
|
|
72
|
+
}
|
|
73
|
+
if (files.length === 0) {
|
|
74
|
+
throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
|
|
75
|
+
}
|
|
76
|
+
const latestFile = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path;
|
|
77
|
+
const fileContent = await fs.readFile(latestFile, 'utf-8');
|
|
78
|
+
const buildInfo = JSON.parse(fileContent);
|
|
79
|
+
const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
|
|
80
|
+
if (!buildOutput) {
|
|
81
|
+
throw new Error(`Build-info file ${latestFile} is missing output data.`);
|
|
82
|
+
}
|
|
83
|
+
// Extract ABIs from contracts
|
|
84
|
+
const contractAbis = new Map();
|
|
85
|
+
if (buildOutput.contracts) {
|
|
86
|
+
for (const [_filePath, contracts] of Object.entries(buildOutput.contracts)) {
|
|
87
|
+
for (const [contractName, contractData] of Object.entries(contracts)) {
|
|
88
|
+
if (contractData.abi) {
|
|
89
|
+
contractAbis.set(contractName, contractData.abi);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const filteredAstData = { ...buildOutput };
|
|
95
|
+
if (filteredAstData.sources) {
|
|
96
|
+
const validSources = {};
|
|
97
|
+
for (const [key, content] of Object.entries(filteredAstData.sources)) {
|
|
98
|
+
const ast = content.ast || content.legacyAST || content.AST;
|
|
99
|
+
if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
|
|
100
|
+
validSources[key] = content;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
filteredAstData.sources = validSources;
|
|
104
|
+
}
|
|
105
|
+
const reader = new solc_typed_ast_1.ASTReader();
|
|
106
|
+
const sourceUnits = reader.read(filteredAstData);
|
|
107
|
+
return { sourceUnits, contractAbis };
|
|
108
|
+
};
|
|
109
|
+
const stripDataLocation = (raw) => {
|
|
110
|
+
if (!raw)
|
|
111
|
+
return '';
|
|
112
|
+
const noLocation = raw
|
|
113
|
+
.replace(/\s+(storage|memory|calldata)(\s+pointer)?/g, '')
|
|
114
|
+
.replace(/\s+pointer/g, '');
|
|
115
|
+
const noPrefix = noLocation
|
|
116
|
+
.replace(/^struct\s+/, '')
|
|
117
|
+
.replace(/^enum\s+/, '')
|
|
118
|
+
.replace(/^contract\s+/, '')
|
|
119
|
+
.trim();
|
|
120
|
+
return noPrefix;
|
|
121
|
+
};
|
|
122
|
+
const signatureFromFnDef = (fnDef) => {
|
|
123
|
+
const params = fnDef.vParameters.vParameters
|
|
124
|
+
.map((p) => { var _a; return stripDataLocation((_a = p.typeString) !== null && _a !== void 0 ? _a : ''); })
|
|
125
|
+
.join(',');
|
|
126
|
+
const name = fnDef.name
|
|
127
|
+
|| (fnDef.kind === solc_typed_ast_1.FunctionKind.Constructor
|
|
128
|
+
? 'constructor'
|
|
129
|
+
: fnDef.kind === solc_typed_ast_1.FunctionKind.Fallback
|
|
130
|
+
? 'fallback'
|
|
131
|
+
: fnDef.kind === solc_typed_ast_1.FunctionKind.Receive
|
|
132
|
+
? 'receive'
|
|
133
|
+
: '');
|
|
134
|
+
return `${name}(${params})`;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Convert typeString to actual Solidity type
|
|
138
|
+
* e.g. "int_const 42" -> "uint256", "bool" -> "bool", etc.
|
|
139
|
+
*/
|
|
140
|
+
const typeStringToSolidityType = (typeString, value) => {
|
|
141
|
+
if (!typeString)
|
|
142
|
+
return 'unknown';
|
|
143
|
+
// Handle int_const and rational_const
|
|
144
|
+
if (typeString.startsWith('int_const')) {
|
|
145
|
+
// Check if the value is negative
|
|
146
|
+
if (value && value.startsWith('-')) {
|
|
147
|
+
return 'int256';
|
|
148
|
+
}
|
|
149
|
+
return 'uint256';
|
|
150
|
+
}
|
|
151
|
+
if (typeString.startsWith('rational_const')) {
|
|
152
|
+
return 'uint256';
|
|
153
|
+
}
|
|
154
|
+
// Handle literal_string
|
|
155
|
+
if (typeString.startsWith('literal_string')) {
|
|
156
|
+
return 'string';
|
|
157
|
+
}
|
|
158
|
+
// Handle basic types
|
|
159
|
+
const basicTypes = [
|
|
160
|
+
'bool', 'address', 'bytes', 'string',
|
|
161
|
+
'uint8', 'uint16', 'uint32', 'uint64', 'uint128', 'uint256',
|
|
162
|
+
'int8', 'int16', 'int32', 'int64', 'int128', 'int256',
|
|
163
|
+
'bytes1', 'bytes2', 'bytes4', 'bytes8', 'bytes16', 'bytes32',
|
|
164
|
+
];
|
|
165
|
+
for (const t of basicTypes) {
|
|
166
|
+
if (typeString === t || typeString.startsWith(t + ' ')) {
|
|
167
|
+
return t;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Default fallback
|
|
171
|
+
return typeString;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Convert literal value for display
|
|
175
|
+
*/
|
|
176
|
+
const formatLiteralValue = (lit) => {
|
|
177
|
+
var _a;
|
|
178
|
+
if (lit.kind === 'bool') {
|
|
179
|
+
return lit.value === 'true' ? 'True' : 'False';
|
|
180
|
+
}
|
|
181
|
+
return (_a = lit.value) !== null && _a !== void 0 ? _a : '';
|
|
182
|
+
};
|
|
183
|
+
const getAllContracts = (sourceUnits) => {
|
|
184
|
+
const contracts = new Map();
|
|
185
|
+
for (const unit of sourceUnits) {
|
|
186
|
+
for (const contract of unit.getChildrenByType(solc_typed_ast_1.ContractDefinition)) {
|
|
187
|
+
contracts.set(contract.name, contract);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return contracts;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Recursively collect all contracts related to the target contract:
|
|
194
|
+
* - Base contracts (inheritance chain)
|
|
195
|
+
* - Contracts referenced in state variables
|
|
196
|
+
* - Contracts called in functions
|
|
197
|
+
*/
|
|
198
|
+
const collectRelatedContracts = (targetContract, allContracts) => {
|
|
199
|
+
const visited = new Set();
|
|
200
|
+
const queue = [targetContract];
|
|
201
|
+
while (queue.length > 0) {
|
|
202
|
+
const contract = queue.shift();
|
|
203
|
+
if (visited.has(contract))
|
|
204
|
+
continue;
|
|
205
|
+
visited.add(contract);
|
|
206
|
+
// 1. Collect from linearized base contracts (inheritance)
|
|
207
|
+
for (const baseContract of contract.vLinearizedBaseContracts) {
|
|
208
|
+
if (!visited.has(baseContract)) {
|
|
209
|
+
queue.push(baseContract);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// 2. Collect from state variable types (contract references)
|
|
213
|
+
const stateVars = (0, utils_1.getDefinitions)(contract, 'vStateVariables', true);
|
|
214
|
+
for (const stateVar of stateVars) {
|
|
215
|
+
// Check if the type references a contract
|
|
216
|
+
const typeNode = stateVar.vType;
|
|
217
|
+
if (typeNode instanceof solc_typed_ast_1.UserDefinedTypeName) {
|
|
218
|
+
const ref = typeNode.vReferencedDeclaration;
|
|
219
|
+
if (ref instanceof solc_typed_ast_1.ContractDefinition && !visited.has(ref)) {
|
|
220
|
+
queue.push(ref);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Also check typeString for contract types
|
|
224
|
+
const typeString = stateVar.typeString;
|
|
225
|
+
if (typeString === null || typeString === void 0 ? void 0 : typeString.startsWith('contract ')) {
|
|
226
|
+
const contractName = typeString.replace('contract ', '');
|
|
227
|
+
const refContract = allContracts.get(contractName);
|
|
228
|
+
if (refContract && !visited.has(refContract)) {
|
|
229
|
+
queue.push(refContract);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// 3. Collect from function calls to other contracts
|
|
234
|
+
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true);
|
|
235
|
+
for (const fnDef of allFunctions) {
|
|
236
|
+
for (const call of fnDef.getChildrenByType(solc_typed_ast_1.FunctionCall)) {
|
|
237
|
+
// Check if call target is on another contract
|
|
238
|
+
const expr = call.vExpression;
|
|
239
|
+
if (expr instanceof solc_typed_ast_1.MemberAccess) {
|
|
240
|
+
const baseExpr = expr.vExpression;
|
|
241
|
+
// Check typeString for contract type
|
|
242
|
+
const typeString = baseExpr.typeString;
|
|
243
|
+
if (typeString === null || typeString === void 0 ? void 0 : typeString.startsWith('contract ')) {
|
|
244
|
+
const contractName = typeString.replace('contract ', '');
|
|
245
|
+
const refContract = allContracts.get(contractName);
|
|
246
|
+
if (refContract && !visited.has(refContract)) {
|
|
247
|
+
queue.push(refContract);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Also check the referenced declaration
|
|
251
|
+
const ref = expr.vReferencedDeclaration;
|
|
252
|
+
if (ref instanceof solc_typed_ast_1.FunctionDefinition) {
|
|
253
|
+
const parentContract = ref.vScope;
|
|
254
|
+
if (parentContract instanceof solc_typed_ast_1.ContractDefinition && !visited.has(parentContract)) {
|
|
255
|
+
queue.push(parentContract);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return visited;
|
|
263
|
+
};
|
|
264
|
+
const collectPayableFunctions = (contract) => {
|
|
265
|
+
const payable = [];
|
|
266
|
+
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
|
|
267
|
+
for (const fnDef of allFunctions) {
|
|
268
|
+
if (fnDef.stateMutability === solc_typed_ast_1.FunctionStateMutability.Payable) {
|
|
269
|
+
if (fnDef.kind === solc_typed_ast_1.FunctionKind.Receive) {
|
|
270
|
+
payable.push('()');
|
|
271
|
+
}
|
|
272
|
+
else if (fnDef.kind === solc_typed_ast_1.FunctionKind.Fallback) {
|
|
273
|
+
payable.push('fallback()');
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
payable.push(signatureFromFnDef(fnDef));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return payable;
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* Collect constant functions from ABI (view/pure functions)
|
|
284
|
+
* This is more reliable than AST for complex types
|
|
285
|
+
*/
|
|
286
|
+
const collectConstantFunctionsFromAbi = (contractName, contractAbis) => {
|
|
287
|
+
const abi = contractAbis.get(contractName);
|
|
288
|
+
if (!abi)
|
|
289
|
+
return [];
|
|
290
|
+
const constants = [];
|
|
291
|
+
for (const entry of abi) {
|
|
292
|
+
if (entry.type === 'function' &&
|
|
293
|
+
(entry.stateMutability === 'view' || entry.stateMutability === 'pure')) {
|
|
294
|
+
const params = (entry.inputs || []).map(inp => inp.type).join(',');
|
|
295
|
+
const sig = `${entry.name}(${params})`;
|
|
296
|
+
constants.push(sig);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return constants;
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Collect constant functions including:
|
|
303
|
+
* - view/pure functions
|
|
304
|
+
* - public state variable getters
|
|
305
|
+
*/
|
|
306
|
+
const collectConstantFunctions = (contract) => {
|
|
307
|
+
const constants = [];
|
|
308
|
+
const seen = new Set();
|
|
309
|
+
// 1. Collect view/pure functions
|
|
310
|
+
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
|
|
311
|
+
for (const fnDef of allFunctions) {
|
|
312
|
+
if (fnDef.visibility === solc_typed_ast_1.FunctionVisibility.External || fnDef.visibility === solc_typed_ast_1.FunctionVisibility.Public) {
|
|
313
|
+
const sig = signatureFromFnDef(fnDef);
|
|
314
|
+
if (!seen.has(sig)) {
|
|
315
|
+
seen.add(sig);
|
|
316
|
+
constants.push(sig);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// 2. Collect public state variable auto-generated getters
|
|
321
|
+
const stateVars = (0, utils_1.getDefinitions)(contract, 'vStateVariables', true);
|
|
322
|
+
for (const stateVar of stateVars) {
|
|
323
|
+
if (stateVar.visibility === 'public') {
|
|
324
|
+
// Generate getter signature based on type
|
|
325
|
+
const sig = generateGetterSignature(stateVar);
|
|
326
|
+
if (sig && !seen.has(sig)) {
|
|
327
|
+
seen.add(sig);
|
|
328
|
+
constants.push(sig);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return constants;
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* Generate getter function signature for public state variables
|
|
336
|
+
*/
|
|
337
|
+
const generateGetterSignature = (stateVar) => {
|
|
338
|
+
const name = stateVar.name;
|
|
339
|
+
const typeString = stateVar.typeString;
|
|
340
|
+
if (!typeString)
|
|
341
|
+
return null;
|
|
342
|
+
// Handle mappings - generate signature with key type
|
|
343
|
+
if (typeString.startsWith('mapping(')) {
|
|
344
|
+
const keyType = extractMappingKeyType(typeString);
|
|
345
|
+
if (keyType) {
|
|
346
|
+
return `${name}(${keyType})`;
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
// Handle arrays - generate signature with uint256 index
|
|
351
|
+
if (typeString.endsWith('[]') || typeString.match(/\[\d+\]$/)) {
|
|
352
|
+
return `${name}(uint256)`;
|
|
353
|
+
}
|
|
354
|
+
// Simple variables - no parameters
|
|
355
|
+
return `${name}()`;
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* Extract the key type from a mapping type string
|
|
359
|
+
*/
|
|
360
|
+
const extractMappingKeyType = (typeString) => {
|
|
361
|
+
// mapping(address => uint256) -> address
|
|
362
|
+
// mapping(uint256 => mapping(address => bool)) -> uint256
|
|
363
|
+
const match = typeString.match(/^mapping\((\w+)\s*=>/);
|
|
364
|
+
if (match) {
|
|
365
|
+
return match[1];
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
};
|
|
369
|
+
/**
|
|
370
|
+
* Collect assert statements with source location info
|
|
371
|
+
*/
|
|
372
|
+
const collectAssertStatements = (contract, foundryRoot, sourceContents) => {
|
|
373
|
+
const asserts = {};
|
|
374
|
+
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
|
|
375
|
+
for (const fnDef of allFunctions) {
|
|
376
|
+
if (fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.External && fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.Public) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (fnDef.kind !== solc_typed_ast_1.FunctionKind.Function && fnDef.kind !== solc_typed_ast_1.FunctionKind.Constructor) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const sig = signatureFromFnDef(fnDef);
|
|
383
|
+
const assertInfos = [];
|
|
384
|
+
for (const call of fnDef.getChildrenByType(solc_typed_ast_1.FunctionCall)) {
|
|
385
|
+
const expr = call.vExpression;
|
|
386
|
+
if (expr instanceof solc_typed_ast_1.Identifier && expr.name === 'assert') {
|
|
387
|
+
// Get source location
|
|
388
|
+
const src = call.src;
|
|
389
|
+
if (src) {
|
|
390
|
+
const [startStr, lengthStr, fileIdStr] = src.split(':');
|
|
391
|
+
const start = parseInt(startStr, 10);
|
|
392
|
+
const length = parseInt(lengthStr, 10);
|
|
393
|
+
// Get the source file path
|
|
394
|
+
const sourceUnit = call.getClosestParentByType(solc_typed_ast_1.SourceUnit);
|
|
395
|
+
let filePath = (sourceUnit === null || sourceUnit === void 0 ? void 0 : sourceUnit.absolutePath) || '';
|
|
396
|
+
// Make path relative to foundry root
|
|
397
|
+
let relPath = filePath;
|
|
398
|
+
if (filePath.startsWith(foundryRoot)) {
|
|
399
|
+
relPath = filePath.slice(foundryRoot.length + 1);
|
|
400
|
+
}
|
|
401
|
+
// Get line info from source content
|
|
402
|
+
const content = sourceContents.get(filePath) || '';
|
|
403
|
+
let lines = [];
|
|
404
|
+
let startingColumn = 1;
|
|
405
|
+
let endingColumn = 1;
|
|
406
|
+
if (content) {
|
|
407
|
+
const lineInfo = (0, utils_1.getLines)(content, src);
|
|
408
|
+
lines = [];
|
|
409
|
+
for (let i = lineInfo.start; i <= lineInfo.end; i++) {
|
|
410
|
+
lines.push(i);
|
|
411
|
+
}
|
|
412
|
+
// Calculate columns
|
|
413
|
+
const beforeStart = content.substring(0, start);
|
|
414
|
+
const lastNewline = beforeStart.lastIndexOf('\n');
|
|
415
|
+
startingColumn = start - lastNewline;
|
|
416
|
+
endingColumn = startingColumn + length;
|
|
417
|
+
}
|
|
418
|
+
assertInfos.push({
|
|
419
|
+
start,
|
|
420
|
+
length,
|
|
421
|
+
filename_relative: relPath,
|
|
422
|
+
filename_absolute: filePath,
|
|
423
|
+
filename_short: relPath,
|
|
424
|
+
is_dependency: filePath.includes('lib/') || filePath.includes('node_modules/'),
|
|
425
|
+
lines,
|
|
426
|
+
starting_column: startingColumn,
|
|
427
|
+
ending_column: endingColumn,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// Only include functions that have asserts
|
|
433
|
+
if (assertInfos.length > 0) {
|
|
434
|
+
asserts[sig] = assertInfos;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return asserts;
|
|
438
|
+
};
|
|
439
|
+
/**
|
|
440
|
+
* Collect constants used in functions with proper type formatting
|
|
441
|
+
* Also includes Z3-solved values from constraint expressions
|
|
442
|
+
*/
|
|
443
|
+
const collectConstantsUsed = async (contract) => {
|
|
444
|
+
var _a;
|
|
445
|
+
const constantsUsed = {};
|
|
446
|
+
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
|
|
447
|
+
for (const fnDef of allFunctions) {
|
|
448
|
+
if (fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.External && fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.Public) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const sig = signatureFromFnDef(fnDef);
|
|
452
|
+
const literals = [];
|
|
453
|
+
// Collect literal constants
|
|
454
|
+
for (const lit of fnDef.getChildrenByType(solc_typed_ast_1.Literal)) {
|
|
455
|
+
const value = formatLiteralValue(lit);
|
|
456
|
+
const type = typeStringToSolidityType((_a = lit.typeString) !== null && _a !== void 0 ? _a : '', value);
|
|
457
|
+
// Each constant is wrapped in an array (matching slither format)
|
|
458
|
+
literals.push([{ value, type }]);
|
|
459
|
+
}
|
|
460
|
+
// Collect Z3-solved constants from constraint expressions
|
|
461
|
+
try {
|
|
462
|
+
const z3Solutions = await (0, z3Solver_1.findZ3Solutions)(fnDef);
|
|
463
|
+
for (const solution of z3Solutions) {
|
|
464
|
+
// Add Z3 solution as a special constant with metadata
|
|
465
|
+
literals.push([{
|
|
466
|
+
value: solution.value,
|
|
467
|
+
type: solution.type,
|
|
468
|
+
}]);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch (e) {
|
|
472
|
+
// Z3 errors are non-fatal, continue without Z3 solutions
|
|
473
|
+
}
|
|
474
|
+
// Only include functions that have constants
|
|
475
|
+
if (literals.length > 0) {
|
|
476
|
+
constantsUsed[sig] = literals;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return constantsUsed;
|
|
480
|
+
};
|
|
481
|
+
const collectFunctionsRelations = (contract) => {
|
|
482
|
+
const relations = {};
|
|
483
|
+
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
|
|
484
|
+
const stateVars = (0, utils_1.getDefinitions)(contract, 'vStateVariables', true);
|
|
485
|
+
// Build a map of state variable IDs for faster lookup
|
|
486
|
+
const stateVarIds = new Set(stateVars.map(v => v.id));
|
|
487
|
+
const stateVarNames = new Set(stateVars.map(v => v.name));
|
|
488
|
+
const fnWritesVar = new Map();
|
|
489
|
+
const fnReadsVar = new Map();
|
|
490
|
+
// Helper to check if a node is part of assignment left-hand side
|
|
491
|
+
const isInAssignmentLHS = (node) => {
|
|
492
|
+
const parentAssignment = node.getClosestParentByType(solc_typed_ast_1.Assignment);
|
|
493
|
+
if (!parentAssignment)
|
|
494
|
+
return false;
|
|
495
|
+
// Walk up from the node to see if it's under the LHS
|
|
496
|
+
let current = node;
|
|
497
|
+
while (current && current !== parentAssignment) {
|
|
498
|
+
if (current === parentAssignment.vLeftHandSide) {
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
current = current.parent;
|
|
502
|
+
}
|
|
503
|
+
return false;
|
|
504
|
+
};
|
|
505
|
+
for (const fnDef of allFunctions) {
|
|
506
|
+
if (fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.External && fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.Public) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const sig = signatureFromFnDef(fnDef);
|
|
510
|
+
const writes = new Set();
|
|
511
|
+
const reads = new Set();
|
|
512
|
+
// Collect writes (assignments to state variables)
|
|
513
|
+
for (const assignment of fnDef.getChildrenByType(solc_typed_ast_1.Assignment)) {
|
|
514
|
+
if ((0, utils_1.isStateVarAssignment)(assignment)) {
|
|
515
|
+
const varDecl = (0, utils_1.getStateVarAssignment)(assignment);
|
|
516
|
+
if (varDecl && stateVarNames.has(varDecl.name)) {
|
|
517
|
+
writes.add(varDecl.name);
|
|
518
|
+
// Compound assignments (+=, -=, etc.) also read
|
|
519
|
+
if (assignment.operator !== '=') {
|
|
520
|
+
reads.add(varDecl.name);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// Collect reads (identifiers referencing state variables)
|
|
526
|
+
for (const id of fnDef.getChildrenByType(solc_typed_ast_1.Identifier)) {
|
|
527
|
+
const ref = id.vReferencedDeclaration;
|
|
528
|
+
if (ref instanceof solc_typed_ast_1.VariableDeclaration && stateVarIds.has(ref.id)) {
|
|
529
|
+
// Check if this identifier is in an assignment LHS
|
|
530
|
+
if (!isInAssignmentLHS(id)) {
|
|
531
|
+
reads.add(ref.name);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
// Even in LHS, compound assignments read
|
|
535
|
+
const parentAssignment = id.getClosestParentByType(solc_typed_ast_1.Assignment);
|
|
536
|
+
if (parentAssignment && parentAssignment.operator !== '=') {
|
|
537
|
+
reads.add(ref.name);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Collect reads from member accesses (for struct fields, mapping accesses)
|
|
543
|
+
for (const ma of fnDef.getChildrenByType(solc_typed_ast_1.MemberAccess)) {
|
|
544
|
+
const ref = ma.vReferencedDeclaration;
|
|
545
|
+
if (ref instanceof solc_typed_ast_1.VariableDeclaration && stateVarIds.has(ref.id)) {
|
|
546
|
+
if (!isInAssignmentLHS(ma)) {
|
|
547
|
+
reads.add(ref.name);
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
const parentAssignment = ma.getClosestParentByType(solc_typed_ast_1.Assignment);
|
|
551
|
+
if (parentAssignment && parentAssignment.operator !== '=') {
|
|
552
|
+
reads.add(ref.name);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
fnWritesVar.set(sig, writes);
|
|
558
|
+
fnReadsVar.set(sig, reads);
|
|
559
|
+
}
|
|
560
|
+
// Build relations
|
|
561
|
+
for (const fnDef of allFunctions) {
|
|
562
|
+
if (fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.External && fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.Public) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const sig = signatureFromFnDef(fnDef);
|
|
566
|
+
const writes = fnWritesVar.get(sig) || new Set();
|
|
567
|
+
const reads = fnReadsVar.get(sig) || new Set();
|
|
568
|
+
const impacts = [];
|
|
569
|
+
const isImpactedBy = [];
|
|
570
|
+
// This function impacts others (including itself) that read variables this function writes
|
|
571
|
+
for (const [otherSig, otherReads] of fnReadsVar) {
|
|
572
|
+
for (const w of writes) {
|
|
573
|
+
if (otherReads.has(w)) {
|
|
574
|
+
if (!impacts.includes(otherSig)) {
|
|
575
|
+
impacts.push(otherSig);
|
|
576
|
+
}
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// This function is impacted by others (including itself) that write variables this function reads
|
|
582
|
+
for (const [otherSig, otherWrites] of fnWritesVar) {
|
|
583
|
+
for (const r of reads) {
|
|
584
|
+
if (otherWrites.has(r)) {
|
|
585
|
+
if (!isImpactedBy.includes(otherSig)) {
|
|
586
|
+
isImpactedBy.push(otherSig);
|
|
587
|
+
}
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Check for single external call (harness wrapper pattern)
|
|
593
|
+
let externalCall;
|
|
594
|
+
const functionCalls = fnDef.getChildrenByType(solc_typed_ast_1.FunctionCall).filter(call => {
|
|
595
|
+
// Only consider regular function calls (not type conversions, struct constructors, etc.)
|
|
596
|
+
if (call.kind !== solc_typed_ast_1.FunctionCallKind.FunctionCall)
|
|
597
|
+
return false;
|
|
598
|
+
// Must be a member access (e.g., foo.bar())
|
|
599
|
+
if (!(call.vExpression instanceof solc_typed_ast_1.MemberAccess))
|
|
600
|
+
return false;
|
|
601
|
+
const memberAccess = call.vExpression;
|
|
602
|
+
const referencedDecl = memberAccess.vReferencedDeclaration;
|
|
603
|
+
// Must reference a function
|
|
604
|
+
if (!(referencedDecl instanceof solc_typed_ast_1.FunctionDefinition))
|
|
605
|
+
return false;
|
|
606
|
+
// The function must be external or public
|
|
607
|
+
if (referencedDecl.visibility !== solc_typed_ast_1.FunctionVisibility.External &&
|
|
608
|
+
referencedDecl.visibility !== solc_typed_ast_1.FunctionVisibility.Public)
|
|
609
|
+
return false;
|
|
610
|
+
// Must be on a contract (not an interface)
|
|
611
|
+
const parentContract = referencedDecl.vScope;
|
|
612
|
+
if (!(parentContract instanceof solc_typed_ast_1.ContractDefinition))
|
|
613
|
+
return false;
|
|
614
|
+
if (parentContract.kind !== solc_typed_ast_1.ContractKind.Contract)
|
|
615
|
+
return false;
|
|
616
|
+
return true;
|
|
617
|
+
});
|
|
618
|
+
// If exactly one external call to a contract, record it
|
|
619
|
+
if (functionCalls.length === 1) {
|
|
620
|
+
const call = functionCalls[0];
|
|
621
|
+
const memberAccess = call.vExpression;
|
|
622
|
+
const referencedFn = memberAccess.vReferencedDeclaration;
|
|
623
|
+
const parentContract = referencedFn.vScope;
|
|
624
|
+
const fnSig = signatureFromFnDef(referencedFn);
|
|
625
|
+
externalCall = `${parentContract.name}::${fnSig}`;
|
|
626
|
+
}
|
|
627
|
+
const relation = {
|
|
628
|
+
impacts,
|
|
629
|
+
is_impacted_by: isImpactedBy
|
|
630
|
+
};
|
|
631
|
+
if (externalCall) {
|
|
632
|
+
relation.external = externalCall;
|
|
633
|
+
}
|
|
634
|
+
relations[sig] = relation;
|
|
635
|
+
}
|
|
636
|
+
return relations;
|
|
637
|
+
};
|
|
638
|
+
/**
|
|
639
|
+
* Get line number from an AST node's src attribute
|
|
640
|
+
*/
|
|
641
|
+
const getLineFromSrc = (src, sourceContents, absolutePath) => {
|
|
642
|
+
const content = sourceContents.get(absolutePath);
|
|
643
|
+
if (!content || !src)
|
|
644
|
+
return null;
|
|
645
|
+
const lineInfo = (0, utils_1.getLines)(content, src);
|
|
646
|
+
return lineInfo.start;
|
|
647
|
+
};
|
|
648
|
+
/**
|
|
649
|
+
* Check if a function call is an external/high-level call (not internal)
|
|
650
|
+
*/
|
|
651
|
+
const isExternalCall = (call) => {
|
|
652
|
+
// High-level calls to external contracts
|
|
653
|
+
if ((0, utils_1.highLevelCall)(call))
|
|
654
|
+
return true;
|
|
655
|
+
// Member access calls like contract.function()
|
|
656
|
+
if (call.vExpression instanceof solc_typed_ast_1.MemberAccess) {
|
|
657
|
+
const memberAccess = call.vExpression;
|
|
658
|
+
const baseType = memberAccess.vExpression.typeString;
|
|
659
|
+
// If base is a contract type, it's external
|
|
660
|
+
if (baseType === null || baseType === void 0 ? void 0 : baseType.startsWith('contract '))
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
};
|
|
665
|
+
/**
|
|
666
|
+
* Collect external call lines from a list of statements
|
|
667
|
+
*/
|
|
668
|
+
const collectExternalCallLines = (statements, sourceContents, absolutePath) => {
|
|
669
|
+
const lines = [];
|
|
670
|
+
for (const stmt of statements) {
|
|
671
|
+
const calls = stmt.getChildrenByType(solc_typed_ast_1.FunctionCall);
|
|
672
|
+
for (const call of calls) {
|
|
673
|
+
if (call.kind === solc_typed_ast_1.FunctionCallKind.FunctionCall && isExternalCall(call)) {
|
|
674
|
+
const line = getLineFromSrc(call.src, sourceContents, absolutePath);
|
|
675
|
+
if (line !== null) {
|
|
676
|
+
const lineStr = line.toString();
|
|
677
|
+
if (!lines.includes(lineStr)) {
|
|
678
|
+
lines.push(lineStr);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return lines;
|
|
685
|
+
};
|
|
686
|
+
/**
|
|
687
|
+
* Build coverage block from a function/modifier body
|
|
688
|
+
*/
|
|
689
|
+
const buildCoverageBlockFromBody = (definition, sourceContents, contract, callTree, visited = new Set()) => {
|
|
690
|
+
const sourceUnit = definition.getClosestParentByType(solc_typed_ast_1.SourceUnit);
|
|
691
|
+
const absolutePath = (sourceUnit === null || sourceUnit === void 0 ? void 0 : sourceUnit.absolutePath) || '';
|
|
692
|
+
const content = sourceContents.get(absolutePath) || '';
|
|
693
|
+
const lines = [];
|
|
694
|
+
const children = [];
|
|
695
|
+
if (!definition.vBody || definition.vBody.vStatements.length === 0) {
|
|
696
|
+
return { lines, absolutePath, children };
|
|
697
|
+
}
|
|
698
|
+
const statements = definition.vBody.vStatements;
|
|
699
|
+
// Add first statement line
|
|
700
|
+
const firstStmt = statements[0];
|
|
701
|
+
if (firstStmt.src && content) {
|
|
702
|
+
const lineInfo = (0, utils_1.getLines)(content, firstStmt.src);
|
|
703
|
+
lines.push(lineInfo.start.toString());
|
|
704
|
+
}
|
|
705
|
+
// Add last statement line (if different from first)
|
|
706
|
+
const lastStmt = statements[statements.length - 1];
|
|
707
|
+
if (lastStmt.src && content && statements.length > 1) {
|
|
708
|
+
const lineInfo = (0, utils_1.getLines)(content, lastStmt.src);
|
|
709
|
+
const lastLine = lineInfo.end.toString();
|
|
710
|
+
if (!lines.includes(lastLine)) {
|
|
711
|
+
lines.push(lastLine);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// Add external call lines
|
|
715
|
+
const externalLines = collectExternalCallLines(statements, sourceContents, absolutePath);
|
|
716
|
+
for (const line of externalLines) {
|
|
717
|
+
if (!lines.includes(line)) {
|
|
718
|
+
lines.push(line);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
// Process nested blocks (if statements, try/catch, etc.)
|
|
722
|
+
for (const stmt of statements) {
|
|
723
|
+
// Handle if statements
|
|
724
|
+
if (stmt instanceof solc_typed_ast_1.IfStatement) {
|
|
725
|
+
const ifChildren = buildCoverageBlockFromIfStatement(stmt, sourceContents, absolutePath);
|
|
726
|
+
children.push(...ifChildren);
|
|
727
|
+
}
|
|
728
|
+
// Handle try statements
|
|
729
|
+
if (stmt instanceof solc_typed_ast_1.TryStatement) {
|
|
730
|
+
const tryChildren = buildCoverageBlockFromTryStatement(stmt, sourceContents, absolutePath);
|
|
731
|
+
children.push(...tryChildren);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// Process children from call tree (internal/modifier calls)
|
|
735
|
+
for (const child of callTree.children) {
|
|
736
|
+
// Skip if already visited (recursive call)
|
|
737
|
+
if (child.isRecursive || visited.has(child.nodeId)) {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
visited.add(child.nodeId);
|
|
741
|
+
const childBlock = buildCoverageBlockFromBody(child.definition, sourceContents, contract, child, visited);
|
|
742
|
+
if (childBlock.lines.length > 0 || childBlock.children.length > 0) {
|
|
743
|
+
children.push(childBlock);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return { lines, absolutePath, children };
|
|
747
|
+
};
|
|
748
|
+
/**
|
|
749
|
+
* Build coverage blocks from an if statement
|
|
750
|
+
*/
|
|
751
|
+
const buildCoverageBlockFromIfStatement = (ifStmt, sourceContents, absolutePath) => {
|
|
752
|
+
const blocks = [];
|
|
753
|
+
const content = sourceContents.get(absolutePath) || '';
|
|
754
|
+
// True branch
|
|
755
|
+
if (ifStmt.vTrueBody) {
|
|
756
|
+
const trueBranch = buildCoverageBlockFromStatementOrBlock(ifStmt.vTrueBody, sourceContents, absolutePath);
|
|
757
|
+
if (trueBranch.lines.length > 0) {
|
|
758
|
+
blocks.push(trueBranch);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// False branch (else)
|
|
762
|
+
if (ifStmt.vFalseBody) {
|
|
763
|
+
const falseBranch = buildCoverageBlockFromStatementOrBlock(ifStmt.vFalseBody, sourceContents, absolutePath);
|
|
764
|
+
if (falseBranch.lines.length > 0) {
|
|
765
|
+
blocks.push(falseBranch);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return blocks;
|
|
769
|
+
};
|
|
770
|
+
/**
|
|
771
|
+
* Build coverage blocks from a try statement
|
|
772
|
+
*/
|
|
773
|
+
const buildCoverageBlockFromTryStatement = (tryStmt, sourceContents, absolutePath) => {
|
|
774
|
+
const blocks = [];
|
|
775
|
+
// Process each catch clause
|
|
776
|
+
for (const clause of tryStmt.vClauses) {
|
|
777
|
+
if (clause.vBlock) {
|
|
778
|
+
const clauseBlock = buildCoverageBlockFromStatementOrBlock(clause.vBlock, sourceContents, absolutePath);
|
|
779
|
+
if (clauseBlock.lines.length > 0) {
|
|
780
|
+
blocks.push(clauseBlock);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return blocks;
|
|
785
|
+
};
|
|
786
|
+
/**
|
|
787
|
+
* Build coverage block from a statement or block
|
|
788
|
+
*/
|
|
789
|
+
const buildCoverageBlockFromStatementOrBlock = (node, sourceContents, absolutePath) => {
|
|
790
|
+
const content = sourceContents.get(absolutePath) || '';
|
|
791
|
+
const lines = [];
|
|
792
|
+
const children = [];
|
|
793
|
+
let statements = [];
|
|
794
|
+
if (node instanceof solc_typed_ast_1.Block) {
|
|
795
|
+
statements = node.vStatements;
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
statements = [node];
|
|
799
|
+
}
|
|
800
|
+
if (statements.length === 0) {
|
|
801
|
+
return { lines, absolutePath, children };
|
|
802
|
+
}
|
|
803
|
+
// Add first statement line
|
|
804
|
+
const firstStmt = statements[0];
|
|
805
|
+
if (firstStmt.src && content) {
|
|
806
|
+
const lineInfo = (0, utils_1.getLines)(content, firstStmt.src);
|
|
807
|
+
lines.push(lineInfo.start.toString());
|
|
808
|
+
}
|
|
809
|
+
// Add last statement line
|
|
810
|
+
const lastStmt = statements[statements.length - 1];
|
|
811
|
+
if (lastStmt.src && content && statements.length > 1) {
|
|
812
|
+
const lineInfo = (0, utils_1.getLines)(content, lastStmt.src);
|
|
813
|
+
const lastLine = lineInfo.end.toString();
|
|
814
|
+
if (!lines.includes(lastLine)) {
|
|
815
|
+
lines.push(lastLine);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
// Add external call lines
|
|
819
|
+
const externalLines = collectExternalCallLines(statements, sourceContents, absolutePath);
|
|
820
|
+
for (const line of externalLines) {
|
|
821
|
+
if (!lines.includes(line)) {
|
|
822
|
+
lines.push(line);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// Process nested blocks
|
|
826
|
+
for (const stmt of statements) {
|
|
827
|
+
if (stmt instanceof solc_typed_ast_1.IfStatement) {
|
|
828
|
+
const ifChildren = buildCoverageBlockFromIfStatement(stmt, sourceContents, absolutePath);
|
|
829
|
+
children.push(...ifChildren);
|
|
830
|
+
}
|
|
831
|
+
if (stmt instanceof solc_typed_ast_1.TryStatement) {
|
|
832
|
+
const tryChildren = buildCoverageBlockFromTryStatement(stmt, sourceContents, absolutePath);
|
|
833
|
+
children.push(...tryChildren);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return { lines, absolutePath, children };
|
|
837
|
+
};
|
|
838
|
+
/**
|
|
839
|
+
* Collect coverage map for a contract
|
|
840
|
+
*/
|
|
841
|
+
const collectCoverageMap = (contract, sourceContents) => {
|
|
842
|
+
const coverageMap = {};
|
|
843
|
+
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
|
|
844
|
+
for (const fnDef of allFunctions) {
|
|
845
|
+
// Only include external/public functions
|
|
846
|
+
if (fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.External && fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.Public) {
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const sig = signatureFromFnDef(fnDef);
|
|
850
|
+
// Build call tree context
|
|
851
|
+
const context = {
|
|
852
|
+
nodeCounter: { value: 0 },
|
|
853
|
+
contract: contract,
|
|
854
|
+
callStack: [],
|
|
855
|
+
activeNodes: new Map()
|
|
856
|
+
};
|
|
857
|
+
// Build call tree for this function
|
|
858
|
+
const callTree = (0, call_tree_builder_1.buildCallTree)(fnDef, [], undefined, undefined, context);
|
|
859
|
+
// Build coverage block from call tree
|
|
860
|
+
const coverageBlock = buildCoverageBlockFromBody(fnDef, sourceContents, contract, callTree);
|
|
861
|
+
coverageMap[sig] = coverageBlock;
|
|
862
|
+
}
|
|
863
|
+
return coverageMap;
|
|
864
|
+
};
|
|
865
|
+
const hasFallback = (contract) => {
|
|
866
|
+
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true);
|
|
867
|
+
return allFunctions.some(fn => fn.kind === solc_typed_ast_1.FunctionKind.Fallback);
|
|
868
|
+
};
|
|
869
|
+
const hasReceive = (contract) => {
|
|
870
|
+
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true);
|
|
871
|
+
return allFunctions.some(fn => fn.kind === solc_typed_ast_1.FunctionKind.Receive);
|
|
872
|
+
};
|
|
873
|
+
/**
|
|
874
|
+
* Load source file contents for source location info
|
|
875
|
+
*/
|
|
876
|
+
const loadSourceContents = async (sourceUnits) => {
|
|
877
|
+
const contents = new Map();
|
|
878
|
+
for (const unit of sourceUnits) {
|
|
879
|
+
const filePath = unit.absolutePath;
|
|
880
|
+
if (filePath) {
|
|
881
|
+
try {
|
|
882
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
883
|
+
contents.set(filePath, content);
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
// File might not exist or not accessible
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return contents;
|
|
891
|
+
};
|
|
892
|
+
const runInfo = async (foundryRoot, contractName, options = {}) => {
|
|
893
|
+
const log = options.json ? () => { } : console.log.bind(console);
|
|
894
|
+
// Set Z3 to silent mode if --json is used
|
|
895
|
+
if (options.json) {
|
|
896
|
+
(0, z3Solver_1.setZ3Silent)(true);
|
|
897
|
+
}
|
|
898
|
+
const { sourceUnits, contractAbis } = await loadLatestBuildInfo(foundryRoot);
|
|
899
|
+
if (!sourceUnits || sourceUnits.length === 0) {
|
|
900
|
+
throw new Error('No source units were produced from the build; cannot generate info.');
|
|
901
|
+
}
|
|
902
|
+
// Load source file contents for assert info
|
|
903
|
+
const sourceContents = await loadSourceContents(sourceUnits);
|
|
904
|
+
const allContracts = getAllContracts(sourceUnits);
|
|
905
|
+
const targetContract = allContracts.get(contractName);
|
|
906
|
+
if (!targetContract) {
|
|
907
|
+
throw new Error(`Contract ${contractName} not found in build output.`);
|
|
908
|
+
}
|
|
909
|
+
// Recursively collect all related contracts
|
|
910
|
+
const relatedContracts = collectRelatedContracts(targetContract, allContracts);
|
|
911
|
+
log(`[recon-generate] Found ${relatedContracts.size} related contracts`);
|
|
912
|
+
const output = {
|
|
913
|
+
payable: {},
|
|
914
|
+
assert: {},
|
|
915
|
+
constant_functions: {},
|
|
916
|
+
constants_used: {},
|
|
917
|
+
functions_relations: {},
|
|
918
|
+
with_fallback: [],
|
|
919
|
+
with_receive: [],
|
|
920
|
+
coverage_map: {},
|
|
921
|
+
};
|
|
922
|
+
// Collect info for all related contracts
|
|
923
|
+
for (const contract of relatedContracts) {
|
|
924
|
+
// Skip abstract contracts (their functions are inherited by concrete contracts)
|
|
925
|
+
// But include libraries (ContractKind.Library) since they contain important logic
|
|
926
|
+
if (contract.abstract) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (contract.kind !== solc_typed_ast_1.ContractKind.Contract && contract.kind !== solc_typed_ast_1.ContractKind.Library) {
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
// Payable functions
|
|
933
|
+
const payable = collectPayableFunctions(contract);
|
|
934
|
+
if (payable.length > 0) {
|
|
935
|
+
output.payable[contract.name] = payable;
|
|
936
|
+
}
|
|
937
|
+
// Constant functions from ABI (more reliable for complex types)
|
|
938
|
+
const constantFns = collectConstantFunctionsFromAbi(contract.name, contractAbis);
|
|
939
|
+
if (constantFns.length > 0) {
|
|
940
|
+
output.constant_functions[contract.name] = constantFns;
|
|
941
|
+
}
|
|
942
|
+
// Assert statements (only include contracts with asserts)
|
|
943
|
+
const asserts = collectAssertStatements(contract, foundryRoot, sourceContents);
|
|
944
|
+
if (Object.keys(asserts).length > 0) {
|
|
945
|
+
output.assert[contract.name] = asserts;
|
|
946
|
+
}
|
|
947
|
+
// Constants used (only include contracts/functions with constants)
|
|
948
|
+
// Also includes Z3-solved values from constraint expressions
|
|
949
|
+
const constants = await collectConstantsUsed(contract);
|
|
950
|
+
if (Object.keys(constants).length > 0) {
|
|
951
|
+
output.constants_used[contract.name] = constants;
|
|
952
|
+
}
|
|
953
|
+
// Functions relations
|
|
954
|
+
const relations = collectFunctionsRelations(contract);
|
|
955
|
+
output.functions_relations[contract.name] = relations;
|
|
956
|
+
// Coverage map
|
|
957
|
+
const coverage = collectCoverageMap(contract, sourceContents);
|
|
958
|
+
if (Object.keys(coverage).length > 0) {
|
|
959
|
+
output.coverage_map[contract.name] = coverage;
|
|
960
|
+
}
|
|
961
|
+
// Fallback and receive
|
|
962
|
+
if (hasFallback(contract)) {
|
|
963
|
+
output.with_fallback.push(contract.name);
|
|
964
|
+
}
|
|
965
|
+
if (hasReceive(contract)) {
|
|
966
|
+
output.with_receive.push(contract.name);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const jsonOutput = JSON.stringify(output, null, 2);
|
|
970
|
+
// Cleanup Z3 context
|
|
971
|
+
await (0, z3Solver_1.cleanupZ3)();
|
|
972
|
+
// Output to terminal if --json flag is set
|
|
973
|
+
if (options.json) {
|
|
974
|
+
console.log(jsonOutput);
|
|
975
|
+
}
|
|
976
|
+
// Write to file if outputPath is provided or --json is not set
|
|
977
|
+
if (options.outputPath || !options.json) {
|
|
978
|
+
const infoPath = options.outputPath || path.join(foundryRoot, `recon-info-${contractName}.json`);
|
|
979
|
+
await fs.writeFile(infoPath, jsonOutput);
|
|
980
|
+
log(`[recon-generate] Wrote info file to ${infoPath}`);
|
|
981
|
+
}
|
|
982
|
+
return output;
|
|
983
|
+
};
|
|
984
|
+
exports.runInfo = runInfo;
|