recon-generate 0.0.38 → 0.0.39
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/db.js +68 -21
- package/dist/link2.js +20 -26
- package/dist/sourcemap.js +67 -17
- package/dist/utils.d.ts +25 -2
- package/dist/utils.js +75 -11
- package/dist/z3Solver.js +42 -25
- package/package.json +6 -6
package/dist/db.js
CHANGED
|
@@ -100,10 +100,19 @@ async function ensureBuild(foundryRoot, forceBuild) {
|
|
|
100
100
|
await runCmd(cmd, foundryRoot);
|
|
101
101
|
}
|
|
102
102
|
/**
|
|
103
|
-
* Load the latest build-info
|
|
103
|
+
* Load the latest build-info cluster from .recon/out/build-info.
|
|
104
|
+
*
|
|
105
|
+
* A single `forge build` run may emit multiple build-info files (one per compiler
|
|
106
|
+
* config). Files are clustered by mtime proximity and their outputs are merged:
|
|
107
|
+
* output.contracts + output.sources + input.sources. First-encountered (newest)
|
|
108
|
+
* file wins on key collisions.
|
|
104
109
|
*/
|
|
105
110
|
function loadBuildInfoRaw(foundryRoot) {
|
|
111
|
+
var _a;
|
|
106
112
|
const buildInfoDir = path.join(foundryRoot, '.recon', 'out', 'build-info');
|
|
113
|
+
// Reuse the shared helper synchronously via readdirSync/statSync so we don't
|
|
114
|
+
// change this function's signature. findLatestBuildInfoCluster is async; the
|
|
115
|
+
// logic is simple enough to inline here for the sync path.
|
|
107
116
|
let files = [];
|
|
108
117
|
try {
|
|
109
118
|
const entries = fs.readdirSync(buildInfoDir);
|
|
@@ -119,28 +128,66 @@ function loadBuildInfoRaw(foundryRoot) {
|
|
|
119
128
|
if (files.length === 0) {
|
|
120
129
|
throw new Error(`No build-info JSON files found in ${buildInfoDir}. Run: forge build --build-info`);
|
|
121
130
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
131
|
+
// Cluster by mtime proximity (10s window) — see utils.findLatestBuildInfoCluster.
|
|
132
|
+
const CLUSTER_WINDOW_MS = 10000;
|
|
133
|
+
const sorted = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
134
|
+
const anchorTime = sorted[0].mtime.getTime();
|
|
135
|
+
const cluster = sorted.filter((f) => anchorTime - f.mtime.getTime() <= CLUSTER_WINDOW_MS);
|
|
136
|
+
if (cluster.length > 1) {
|
|
137
|
+
console.log(`[recon-generate] Detected ${cluster.length} build-info files from latest run; merging.`);
|
|
138
|
+
}
|
|
139
|
+
const mergedOutput = { contracts: {}, sources: {} };
|
|
140
|
+
const mergedInputSources = {};
|
|
141
|
+
let compilerVersion = null;
|
|
142
|
+
for (const file of cluster) {
|
|
143
|
+
console.log(`[recon-generate] Using build-info: ${file.name}`);
|
|
144
|
+
const stat = fs.statSync(file.fullPath);
|
|
145
|
+
console.log(`[recon-generate] Reading build-info (${(stat.size / 1024 / 1024).toFixed(1)} MB)...`);
|
|
146
|
+
const rawContent = fs.readFileSync(file.fullPath, 'utf-8');
|
|
147
|
+
console.log(`[recon-generate] Parsing JSON...`);
|
|
148
|
+
const parsed = JSON.parse(rawContent);
|
|
149
|
+
const { output, compilerVersion: detectedVersion } = normalizeCompilerOutput(parsed);
|
|
150
|
+
// First file (newest) wins on per-key collisions.
|
|
151
|
+
if (output === null || output === void 0 ? void 0 : output.contracts) {
|
|
152
|
+
for (const [filePath, contracts] of Object.entries(output.contracts)) {
|
|
153
|
+
mergedOutput.contracts[filePath] = {
|
|
154
|
+
...contracts,
|
|
155
|
+
...(mergedOutput.contracts[filePath] || {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (output === null || output === void 0 ? void 0 : output.sources) {
|
|
160
|
+
for (const [filePath, src] of Object.entries(output.sources)) {
|
|
161
|
+
if (!mergedOutput.sources[filePath]) {
|
|
162
|
+
mergedOutput.sources[filePath] = src;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if ((_a = parsed.input) === null || _a === void 0 ? void 0 : _a.sources) {
|
|
167
|
+
for (const [filePath, entry] of Object.entries(parsed.input.sources)) {
|
|
168
|
+
if (!mergedInputSources[filePath]) {
|
|
169
|
+
mergedInputSources[filePath] = entry;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!compilerVersion && detectedVersion) {
|
|
174
|
+
compilerVersion = detectedVersion;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
output: mergedOutput,
|
|
179
|
+
inputSources: mergedInputSources,
|
|
180
|
+
compilerVersion: compilerVersion || '0.8.0',
|
|
181
|
+
};
|
|
132
182
|
}
|
|
133
183
|
/**
|
|
134
184
|
* Extract source contents from build-info input.sources
|
|
135
185
|
*/
|
|
136
|
-
function extractSourceContents(
|
|
137
|
-
var _a;
|
|
186
|
+
function extractSourceContents(inputSources) {
|
|
138
187
|
const sourceContents = new Map();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
sourceContents.set(filePath, entry.content);
|
|
143
|
-
}
|
|
188
|
+
for (const [filePath, entry] of Object.entries(inputSources)) {
|
|
189
|
+
if (entry === null || entry === void 0 ? void 0 : entry.content) {
|
|
190
|
+
sourceContents.set(filePath, entry.content);
|
|
144
191
|
}
|
|
145
192
|
}
|
|
146
193
|
return sourceContents;
|
|
@@ -148,9 +195,9 @@ function extractSourceContents(raw) {
|
|
|
148
195
|
const runDb = async (foundryRoot, options = {}) => {
|
|
149
196
|
// Build into .recon/out (skipping test/script), reuse if already present
|
|
150
197
|
await ensureBuild(foundryRoot, !!options.forceBuild);
|
|
151
|
-
// Load latest build-info from .recon/out
|
|
152
|
-
const {
|
|
153
|
-
const sourceContents = extractSourceContents(
|
|
198
|
+
// Load latest build-info cluster from .recon/out
|
|
199
|
+
const { output, inputSources, compilerVersion } = loadBuildInfoRaw(foundryRoot);
|
|
200
|
+
const sourceContents = extractSourceContents(inputSources);
|
|
154
201
|
console.log(`[recon-generate] Compiler version: ${compilerVersion}`);
|
|
155
202
|
if (sourceContents.size > 0) {
|
|
156
203
|
console.log(`[recon-generate] Found ${sourceContents.size} source content entries`);
|
package/dist/link2.js
CHANGED
|
@@ -121,40 +121,34 @@ const runForgeBuildAndGetLibraries = async (cwd, verbose = false) => {
|
|
|
121
121
|
resolve();
|
|
122
122
|
});
|
|
123
123
|
});
|
|
124
|
-
// Find the
|
|
124
|
+
// Find the latest build-info cluster — a single `forge build` run can emit
|
|
125
|
+
// multiple files (one per compiler config), so we stream libraries from all
|
|
126
|
+
// of them and union the results.
|
|
125
127
|
const foundryConfig = await (0, utils_1.getFoundryConfig)(cwd);
|
|
126
128
|
const buildInfoDir = path.join(cwd, foundryConfig.out, 'build-info');
|
|
127
129
|
if (verbose) {
|
|
128
130
|
console.log(`[VERBOSE] Looking for build-info files in: ${buildInfoDir}`);
|
|
129
131
|
}
|
|
130
|
-
let
|
|
132
|
+
let cluster;
|
|
131
133
|
try {
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
throw new Error(`Build info directory not found: ${buildInfoDir}. Make sure forge build --build-info completed successfully.`);
|
|
136
|
-
}
|
|
137
|
-
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
138
|
-
if (jsonFiles.length === 0) {
|
|
139
|
-
throw new Error(`No build-info JSON files found in ${buildInfoDir}`);
|
|
140
|
-
}
|
|
141
|
-
// Get the most recent file by modification time
|
|
142
|
-
let mostRecentFile = jsonFiles[0];
|
|
143
|
-
let mostRecentTime = 0;
|
|
144
|
-
for (const file of jsonFiles) {
|
|
145
|
-
const filePath = path.join(buildInfoDir, file);
|
|
146
|
-
const stats = await fs.stat(filePath);
|
|
147
|
-
if (stats.mtimeMs > mostRecentTime) {
|
|
148
|
-
mostRecentTime = stats.mtimeMs;
|
|
149
|
-
mostRecentFile = file;
|
|
150
|
-
}
|
|
134
|
+
cluster = await (0, utils_1.findLatestBuildInfoCluster)(buildInfoDir);
|
|
151
135
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
136
|
+
catch (e) {
|
|
137
|
+
throw new Error(`${e.message} Make sure forge build --build-info completed successfully.`);
|
|
138
|
+
}
|
|
139
|
+
if (verbose && cluster.length > 1) {
|
|
140
|
+
console.log(`[VERBOSE] Detected ${cluster.length} build-info files from latest run; streaming each.`);
|
|
141
|
+
}
|
|
142
|
+
const allLibraries = new Set();
|
|
143
|
+
for (const file of cluster) {
|
|
144
|
+
if (verbose) {
|
|
145
|
+
console.log(`[VERBOSE] Streaming build-info from: ${file.path}`);
|
|
146
|
+
}
|
|
147
|
+
const libs = await streamLibrariesFromBuildInfo(file.path, verbose);
|
|
148
|
+
for (const lib of libs)
|
|
149
|
+
allLibraries.add(lib);
|
|
155
150
|
}
|
|
156
|
-
|
|
157
|
-
return streamLibrariesFromBuildInfo(buildInfoPath, verbose);
|
|
151
|
+
return Array.from(allLibraries);
|
|
158
152
|
};
|
|
159
153
|
const formatEchidnaYaml = (config, libraries) => {
|
|
160
154
|
const cfg = { ...config };
|
package/dist/sourcemap.js
CHANGED
|
@@ -70,26 +70,63 @@ function findFoundryProject(startDir = process.cwd()) {
|
|
|
70
70
|
}
|
|
71
71
|
return null;
|
|
72
72
|
}
|
|
73
|
-
function
|
|
73
|
+
async function findBuildInfoCluster(projectDir, outDir = 'out') {
|
|
74
74
|
const buildInfoDir = path.join(projectDir, outDir, 'build-info');
|
|
75
75
|
if (!fs.existsSync(buildInfoDir))
|
|
76
76
|
return null;
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
try {
|
|
78
|
+
return await (0, utils_1.findLatestBuildInfoCluster)(buildInfoDir);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
79
81
|
return null;
|
|
80
|
-
|
|
81
|
-
const sorted = files.sort((a, b) => {
|
|
82
|
-
const statA = fs.statSync(path.join(buildInfoDir, a));
|
|
83
|
-
const statB = fs.statSync(path.join(buildInfoDir, b));
|
|
84
|
-
return statB.mtimeMs - statA.mtimeMs;
|
|
85
|
-
});
|
|
86
|
-
return path.join(buildInfoDir, sorted[0]);
|
|
82
|
+
}
|
|
87
83
|
}
|
|
88
84
|
function loadBuildInfo(buildInfoPath) {
|
|
89
85
|
const content = fs.readFileSync(buildInfoPath, 'utf-8');
|
|
90
86
|
const buildInfo = JSON.parse(content);
|
|
91
87
|
return buildInfo.output || buildInfo;
|
|
92
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Load and merge build-info output from every file in the cluster.
|
|
91
|
+
* First-encountered (newest) file wins on per-key collisions in sources/contracts.
|
|
92
|
+
*/
|
|
93
|
+
function loadAndMergeBuildInfoCluster(cluster) {
|
|
94
|
+
const mergedSources = {};
|
|
95
|
+
const mergedContracts = {};
|
|
96
|
+
const sourceListSet = new Set();
|
|
97
|
+
const paths = [];
|
|
98
|
+
for (const file of cluster) {
|
|
99
|
+
const output = loadBuildInfo(file.path);
|
|
100
|
+
paths.push(file.path);
|
|
101
|
+
if (output === null || output === void 0 ? void 0 : output.sources) {
|
|
102
|
+
for (const [filePath, src] of Object.entries(output.sources)) {
|
|
103
|
+
if (!mergedSources[filePath]) {
|
|
104
|
+
mergedSources[filePath] = src;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (output === null || output === void 0 ? void 0 : output.contracts) {
|
|
109
|
+
for (const [filePath, contracts] of Object.entries(output.contracts)) {
|
|
110
|
+
mergedContracts[filePath] = {
|
|
111
|
+
...contracts,
|
|
112
|
+
...(mergedContracts[filePath] || {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(output === null || output === void 0 ? void 0 : output.sourceList)) {
|
|
117
|
+
for (const s of output.sourceList)
|
|
118
|
+
sourceListSet.add(s);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
astData: {
|
|
123
|
+
sources: mergedSources,
|
|
124
|
+
contracts: mergedContracts,
|
|
125
|
+
sourceList: Array.from(sourceListSet),
|
|
126
|
+
},
|
|
127
|
+
paths,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
93
130
|
function getAllSourceContracts(sourceUnits, astData) {
|
|
94
131
|
const results = [];
|
|
95
132
|
const sources = astData.sources || {};
|
|
@@ -158,14 +195,19 @@ async function runSourcemap(foundryRoot, options = {}) {
|
|
|
158
195
|
console.log(`📁 Project: ${projectDir}`);
|
|
159
196
|
console.log(`📂 Output: ${outputDir}`);
|
|
160
197
|
const startTime = Date.now();
|
|
161
|
-
// Find build
|
|
162
|
-
const
|
|
163
|
-
if (!
|
|
198
|
+
// Find build-info cluster (handles multi-config runs emitting >1 file).
|
|
199
|
+
const cluster = await findBuildInfoCluster(projectDir, forgeOutDir);
|
|
200
|
+
if (!cluster || cluster.length === 0) {
|
|
164
201
|
throw new Error(`No build-info found in ${projectDir}/${forgeOutDir}/build-info/. Run "forge build" first to generate build artifacts.`);
|
|
165
202
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
203
|
+
if (cluster.length > 1) {
|
|
204
|
+
console.log(`📂 Found ${cluster.length} build-info files (latest run): ${cluster.map(f => f.name).join(', ')}`);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
console.log(`📂 Found build-info: ${cluster[0].name}`);
|
|
208
|
+
}
|
|
209
|
+
// Load and merge AST + contracts across the cluster.
|
|
210
|
+
const { astData, paths: buildInfoPaths } = loadAndMergeBuildInfoCluster(cluster);
|
|
169
211
|
const reader = new solc_typed_ast_1.ASTReader();
|
|
170
212
|
const skipPatterns = ['forge-std/', 'lib/forge-std/'];
|
|
171
213
|
const filteredSources = {};
|
|
@@ -228,7 +270,15 @@ async function runSourcemap(foundryRoot, options = {}) {
|
|
|
228
270
|
console.log(`🗺️ Step 2/3: Generating Branch Mappings...`);
|
|
229
271
|
console.log(`${'─'.repeat(60)}`);
|
|
230
272
|
const { loadBuildInfoSourceMaps, generateBranchMapping } = await Promise.resolve().then(() => __importStar(require('./cfg/sourcemap')));
|
|
231
|
-
|
|
273
|
+
// Merge source maps from every cluster file. First-encountered (newest) wins.
|
|
274
|
+
const sourceMaps = new Map();
|
|
275
|
+
for (const p of buildInfoPaths) {
|
|
276
|
+
const perFileMaps = loadBuildInfoSourceMaps(p);
|
|
277
|
+
for (const [name, data] of perFileMaps) {
|
|
278
|
+
if (!sourceMaps.has(name))
|
|
279
|
+
sourceMaps.set(name, data);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
232
282
|
const contractsToProcess = Array.from(sourceMaps.keys()).filter(name => {
|
|
233
283
|
const mapData = sourceMaps.get(name);
|
|
234
284
|
return mapData && mapData.sourceFile.startsWith('src/');
|
package/dist/utils.d.ts
CHANGED
|
@@ -65,15 +65,38 @@ export declare const parseBuildOutput: (buildOutput: any, skipPatterns?: string[
|
|
|
65
65
|
* @returns Object containing sourceUnits and raw buildOutput
|
|
66
66
|
*/
|
|
67
67
|
export declare const loadBuildInfoFromFile: (buildInfoPath: string, skipPatterns?: string[]) => Promise<BuildInfoResult>;
|
|
68
|
+
/**
|
|
69
|
+
* A single build-info file on disk.
|
|
70
|
+
*/
|
|
71
|
+
export interface BuildInfoFile {
|
|
72
|
+
name: string;
|
|
73
|
+
path: string;
|
|
74
|
+
mtime: Date;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Find the cluster of build-info files produced by the latest `forge build` run.
|
|
78
|
+
*
|
|
79
|
+
* Foundry emits one build-info per unique compiler config (e.g. mixed solc
|
|
80
|
+
* versions), so a single run can produce multiple files. We anchor on the
|
|
81
|
+
* newest file's mtime and include any file within `clusterWindowMs`. Older
|
|
82
|
+
* files (from prior runs) are excluded. Returns newest-first ordering.
|
|
83
|
+
*/
|
|
84
|
+
export declare const findLatestBuildInfoCluster: (buildInfoDir: string, clusterWindowMs?: number) => Promise<BuildInfoFile[]>;
|
|
68
85
|
/**
|
|
69
86
|
* Load the latest build info from a Foundry project.
|
|
70
87
|
*
|
|
88
|
+
* A single `forge build` run may emit multiple build-info files (one per compiler
|
|
89
|
+
* config — e.g. mixed solc versions). We cluster files by mtime proximity: the
|
|
90
|
+
* newest file anchors a window, and any file with an mtime within `clusterWindowMs`
|
|
91
|
+
* is considered part of the same run. Older files (from prior runs) are ignored.
|
|
92
|
+
*
|
|
71
93
|
* @param foundryRoot - The root directory of the Foundry project
|
|
72
94
|
* @param outputDir - The output directory relative to foundryRoot (default: 'out')
|
|
73
95
|
* @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
|
|
74
|
-
* @
|
|
96
|
+
* @param clusterWindowMs - Time window (ms) for grouping build-infos from the same run (default: 10000)
|
|
97
|
+
* @returns Object containing merged sourceUnits and raw buildOutput
|
|
75
98
|
*/
|
|
76
|
-
export declare const loadLatestBuildInfo: (foundryRoot: string, outputDir?: string, skipPatterns?: string[]) => Promise<BuildInfoResult>;
|
|
99
|
+
export declare const loadLatestBuildInfo: (foundryRoot: string, outputDir?: string, skipPatterns?: string[], clusterWindowMs?: number) => Promise<BuildInfoResult>;
|
|
77
100
|
/**
|
|
78
101
|
* Recursively walks parameter internalType fields and extracts referenced type names.
|
|
79
102
|
* For dotted types like "struct ISpoke.DynamicReserveConfig" → extracts "ISpoke"
|
package/dist/utils.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.loadLatestBuildInfo = exports.loadBuildInfoFromFile = exports.parseBuildOutput = exports.signatureEquals = exports.getFunctionName = exports.getIndex = exports.getSource = exports.getLines = exports.FOUNDRY_CONFIG_DEFAULTS = void 0;
|
|
39
|
+
exports.loadLatestBuildInfo = exports.findLatestBuildInfoCluster = exports.loadBuildInfoFromFile = exports.parseBuildOutput = exports.signatureEquals = exports.getFunctionName = exports.getIndex = exports.getSource = exports.getLines = exports.FOUNDRY_CONFIG_DEFAULTS = void 0;
|
|
40
40
|
exports.fileExists = fileExists;
|
|
41
41
|
exports.getFoundryConfigPath = getFoundryConfigPath;
|
|
42
42
|
exports.findOutputDirectory = findOutputDirectory;
|
|
@@ -527,16 +527,14 @@ const loadBuildInfoFromFile = async (buildInfoPath, skipPatterns = []) => {
|
|
|
527
527
|
};
|
|
528
528
|
exports.loadBuildInfoFromFile = loadBuildInfoFromFile;
|
|
529
529
|
/**
|
|
530
|
-
*
|
|
530
|
+
* Find the cluster of build-info files produced by the latest `forge build` run.
|
|
531
531
|
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
532
|
+
* Foundry emits one build-info per unique compiler config (e.g. mixed solc
|
|
533
|
+
* versions), so a single run can produce multiple files. We anchor on the
|
|
534
|
+
* newest file's mtime and include any file within `clusterWindowMs`. Older
|
|
535
|
+
* files (from prior runs) are excluded. Returns newest-first ordering.
|
|
536
536
|
*/
|
|
537
|
-
const
|
|
538
|
-
const outDir = path.join(foundryRoot, outputDir);
|
|
539
|
-
const buildInfoDir = path.join(outDir, 'build-info');
|
|
537
|
+
const findLatestBuildInfoCluster = async (buildInfoDir, clusterWindowMs = 10000) => {
|
|
540
538
|
let files = [];
|
|
541
539
|
try {
|
|
542
540
|
const entries = await fs.readdir(buildInfoDir);
|
|
@@ -553,8 +551,74 @@ const loadLatestBuildInfo = async (foundryRoot, outputDir = 'out', skipPatterns
|
|
|
553
551
|
if (files.length === 0) {
|
|
554
552
|
throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
|
|
555
553
|
}
|
|
556
|
-
const
|
|
557
|
-
|
|
554
|
+
const sorted = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
555
|
+
const anchorTime = sorted[0].mtime.getTime();
|
|
556
|
+
return sorted.filter((f) => anchorTime - f.mtime.getTime() <= clusterWindowMs);
|
|
557
|
+
};
|
|
558
|
+
exports.findLatestBuildInfoCluster = findLatestBuildInfoCluster;
|
|
559
|
+
/**
|
|
560
|
+
* Merge two build outputs by shallow-combining their `contracts` and `sources` maps.
|
|
561
|
+
* Keys present in `base` are preserved (base wins on collision).
|
|
562
|
+
*/
|
|
563
|
+
const mergeBuildOutputs = (base, next) => {
|
|
564
|
+
const merged = { ...base };
|
|
565
|
+
if (next === null || next === void 0 ? void 0 : next.contracts) {
|
|
566
|
+
merged.contracts = { ...((base === null || base === void 0 ? void 0 : base.contracts) || {}) };
|
|
567
|
+
for (const [filePath, contracts] of Object.entries(next.contracts)) {
|
|
568
|
+
merged.contracts[filePath] = {
|
|
569
|
+
...contracts,
|
|
570
|
+
...(merged.contracts[filePath] || {}),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (next === null || next === void 0 ? void 0 : next.sources) {
|
|
575
|
+
merged.sources = { ...((base === null || base === void 0 ? void 0 : base.sources) || {}) };
|
|
576
|
+
for (const [filePath, src] of Object.entries(next.sources)) {
|
|
577
|
+
if (!merged.sources[filePath]) {
|
|
578
|
+
merged.sources[filePath] = src;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return merged;
|
|
583
|
+
};
|
|
584
|
+
/**
|
|
585
|
+
* Load the latest build info from a Foundry project.
|
|
586
|
+
*
|
|
587
|
+
* A single `forge build` run may emit multiple build-info files (one per compiler
|
|
588
|
+
* config — e.g. mixed solc versions). We cluster files by mtime proximity: the
|
|
589
|
+
* newest file anchors a window, and any file with an mtime within `clusterWindowMs`
|
|
590
|
+
* is considered part of the same run. Older files (from prior runs) are ignored.
|
|
591
|
+
*
|
|
592
|
+
* @param foundryRoot - The root directory of the Foundry project
|
|
593
|
+
* @param outputDir - The output directory relative to foundryRoot (default: 'out')
|
|
594
|
+
* @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
|
|
595
|
+
* @param clusterWindowMs - Time window (ms) for grouping build-infos from the same run (default: 10000)
|
|
596
|
+
* @returns Object containing merged sourceUnits and raw buildOutput
|
|
597
|
+
*/
|
|
598
|
+
const loadLatestBuildInfo = async (foundryRoot, outputDir = 'out', skipPatterns = [], clusterWindowMs = 10000) => {
|
|
599
|
+
const outDir = path.join(foundryRoot, outputDir);
|
|
600
|
+
const buildInfoDir = path.join(outDir, 'build-info');
|
|
601
|
+
const cluster = await (0, exports.findLatestBuildInfoCluster)(buildInfoDir, clusterWindowMs);
|
|
602
|
+
if (cluster.length === 1) {
|
|
603
|
+
return (0, exports.loadBuildInfoFromFile)(cluster[0].path, skipPatterns);
|
|
604
|
+
}
|
|
605
|
+
// Parse each build-info independently (each has its own AST ID space; feeding
|
|
606
|
+
// merged sources into a single ASTReader would collide on duplicate IDs).
|
|
607
|
+
// Then concat sourceUnits (deduped by absolutePath) and merge buildOutput maps.
|
|
608
|
+
const seenSourcePaths = new Set();
|
|
609
|
+
const mergedSourceUnits = [];
|
|
610
|
+
let mergedBuildOutput = {};
|
|
611
|
+
for (const f of cluster) {
|
|
612
|
+
const { sourceUnits, buildOutput } = await (0, exports.loadBuildInfoFromFile)(f.path, skipPatterns);
|
|
613
|
+
for (const unit of sourceUnits) {
|
|
614
|
+
if (!seenSourcePaths.has(unit.absolutePath)) {
|
|
615
|
+
seenSourcePaths.add(unit.absolutePath);
|
|
616
|
+
mergedSourceUnits.push(unit);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
mergedBuildOutput = mergeBuildOutputs(mergedBuildOutput, buildOutput);
|
|
620
|
+
}
|
|
621
|
+
return { sourceUnits: mergedSourceUnits, buildOutput: mergedBuildOutput };
|
|
558
622
|
};
|
|
559
623
|
exports.loadLatestBuildInfo = loadLatestBuildInfo;
|
|
560
624
|
// --- Extra import resolution utilities ---
|
package/dist/z3Solver.js
CHANGED
|
@@ -125,18 +125,17 @@ const getExpressionName = (expr) => {
|
|
|
125
125
|
};
|
|
126
126
|
const expressionToZ3 = (ctx, expr, varMap, bitSize, signed) => {
|
|
127
127
|
if (expr instanceof solc_typed_ast_1.Literal) {
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
// Only numeric literals can be solved as BitVecs. Bool/string/hexString
|
|
129
|
+
// literals have no BitVec form — skip them instead of letting BigInt throw.
|
|
130
|
+
if (expr.kind !== 'number')
|
|
131
|
+
return null;
|
|
132
|
+
const value = (expr.value || '').trim();
|
|
133
|
+
if (!/^-?(0x[0-9a-fA-F]+|[0-9]+)$/.test(value))
|
|
134
|
+
return null;
|
|
134
135
|
if (value.startsWith('-')) {
|
|
135
136
|
const absVal = BigInt(value.slice(1));
|
|
136
|
-
// Two's complement
|
|
137
137
|
const maxVal = BigInt(1) << BigInt(bitSize);
|
|
138
|
-
|
|
139
|
-
return ctx.BitVec.val(signedVal, bitSize);
|
|
138
|
+
return ctx.BitVec.val(maxVal - absVal, bitSize);
|
|
140
139
|
}
|
|
141
140
|
return ctx.BitVec.val(BigInt(value), bitSize);
|
|
142
141
|
}
|
|
@@ -252,19 +251,26 @@ const createZ3Constraint = (ctx, expr, varMap, bitSize, signed) => {
|
|
|
252
251
|
}
|
|
253
252
|
};
|
|
254
253
|
/**
|
|
255
|
-
* Parse Z3 value string to BigInt
|
|
256
|
-
* Z3 returns hex values in format like #xFFFF... or decimal strings
|
|
254
|
+
* Parse Z3 value string to BigInt.
|
|
255
|
+
* Z3 returns hex values in format like #xFFFF... or decimal strings.
|
|
256
|
+
* Returns null for unparseable values (e.g., uninterpreted-function expressions
|
|
257
|
+
* like `keccak256` or `unknown_length` that Z3 couldn't reduce to a literal).
|
|
257
258
|
*/
|
|
258
259
|
const parseZ3Value = (valueStr) => {
|
|
259
|
-
// Handle Z3 hex format: #xFFFF... -> 0xFFFF...
|
|
260
260
|
if (valueStr.startsWith('#x')) {
|
|
261
|
-
|
|
261
|
+
const hex = valueStr.slice(2);
|
|
262
|
+
if (!/^[0-9a-fA-F]+$/.test(hex))
|
|
263
|
+
return null;
|
|
264
|
+
return BigInt('0x' + hex);
|
|
262
265
|
}
|
|
263
|
-
// Handle Z3 binary format: #b0101... -> 0b0101...
|
|
264
266
|
if (valueStr.startsWith('#b')) {
|
|
265
|
-
|
|
267
|
+
const bin = valueStr.slice(2);
|
|
268
|
+
if (!/^[01]+$/.test(bin))
|
|
269
|
+
return null;
|
|
270
|
+
return BigInt('0b' + bin);
|
|
266
271
|
}
|
|
267
|
-
|
|
272
|
+
if (!/^-?(0x[0-9a-fA-F]+|[0-9]+)$/.test(valueStr))
|
|
273
|
+
return null;
|
|
268
274
|
return BigInt(valueStr);
|
|
269
275
|
};
|
|
270
276
|
const solveConstraint = async (ctx, constraint, varMap, bitSize, signed) => {
|
|
@@ -279,6 +285,9 @@ const solveConstraint = async (ctx, constraint, varMap, bitSize, signed) => {
|
|
|
279
285
|
const value = model.eval(z3Var);
|
|
280
286
|
const valueStr = value.toString();
|
|
281
287
|
let numValue = parseZ3Value(valueStr);
|
|
288
|
+
// Skip uninterpreted/non-numeric model values (keccak256(...), etc.)
|
|
289
|
+
if (numValue === null)
|
|
290
|
+
continue;
|
|
282
291
|
// Convert to signed if needed
|
|
283
292
|
if (signed && numValue >= BigInt(1) << BigInt(bitSize - 1)) {
|
|
284
293
|
numValue = numValue - (BigInt(1) << BigInt(bitSize));
|
|
@@ -295,11 +304,19 @@ const setZ3Silent = (silent) => {
|
|
|
295
304
|
exports.setZ3Silent = setZ3Silent;
|
|
296
305
|
const findZ3Solutions = async (fnDef) => {
|
|
297
306
|
const solutions = [];
|
|
307
|
+
let ctx;
|
|
298
308
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
309
|
+
({ ctx } = await initZ3());
|
|
310
|
+
}
|
|
311
|
+
catch (e) {
|
|
312
|
+
if (!silentMode) {
|
|
313
|
+
console.warn(`[z3] Warning: init failed: ${e}`);
|
|
314
|
+
}
|
|
315
|
+
return solutions;
|
|
316
|
+
}
|
|
317
|
+
const binOps = fnDef.getChildrenByType(solc_typed_ast_1.BinaryOperation);
|
|
318
|
+
for (const binOp of binOps) {
|
|
319
|
+
try {
|
|
303
320
|
// Only handle equality operator
|
|
304
321
|
if (binOp.operator !== '==') {
|
|
305
322
|
continue;
|
|
@@ -363,11 +380,11 @@ const findZ3Solutions = async (fnDef) => {
|
|
|
363
380
|
});
|
|
364
381
|
}
|
|
365
382
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
383
|
+
catch (e) {
|
|
384
|
+
// Per-constraint errors are non-fatal — skip this binOp and continue.
|
|
385
|
+
if (!silentMode) {
|
|
386
|
+
console.warn(`[z3] Warning: ${e}`);
|
|
387
|
+
}
|
|
371
388
|
}
|
|
372
389
|
}
|
|
373
390
|
return solutions;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recon-generate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.39",
|
|
4
4
|
"description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
"commander": "^14.0.3",
|
|
27
27
|
"ethers": "^6.16.0",
|
|
28
28
|
"handlebars": "^4.7.8",
|
|
29
|
-
"solc-typed-ast": "^20.0.
|
|
29
|
+
"solc-typed-ast": "^20.0.5",
|
|
30
30
|
"src-location": "^1.1.0",
|
|
31
31
|
"stream-json": "^1.9.1",
|
|
32
|
-
"better-sqlite3": "^12.
|
|
33
|
-
"yaml": "^2.8.
|
|
34
|
-
"z3-solver": "^4.
|
|
32
|
+
"better-sqlite3": "^12.9.0",
|
|
33
|
+
"yaml": "^2.8.3",
|
|
34
|
+
"z3-solver": "^4.16.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@types/node": "^25.
|
|
37
|
+
"@types/node": "^25.6.0",
|
|
38
38
|
"@types/stream-json": "^1.7.8",
|
|
39
39
|
"typescript": "^5.8.3"
|
|
40
40
|
}
|