recon-generate 0.0.26 → 0.0.28

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/coverage.js CHANGED
@@ -85,47 +85,6 @@ const runCmd = (cmd, cwd) => {
85
85
  });
86
86
  });
87
87
  };
88
- const loadLatestSourceUnits = async (foundryRoot) => {
89
- var _a;
90
- const outDir = path.join(foundryRoot, '.recon', 'out');
91
- const buildInfoDir = path.join(outDir, 'build-info');
92
- let files = [];
93
- try {
94
- const entries = await fs.readdir(buildInfoDir);
95
- const jsonFiles = entries.filter((f) => f.endsWith('.json'));
96
- files = await Promise.all(jsonFiles.map(async (f) => ({
97
- name: f,
98
- path: path.join(buildInfoDir, f),
99
- mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime,
100
- })));
101
- }
102
- catch (e) {
103
- throw new Error(`No build-info directory found at ${buildInfoDir}: ${e}`);
104
- }
105
- if (files.length === 0) {
106
- throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
107
- }
108
- const latestFile = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path;
109
- const fileContent = await fs.readFile(latestFile, 'utf-8');
110
- const buildInfo = JSON.parse(fileContent);
111
- const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
112
- if (!buildOutput) {
113
- throw new Error(`Build-info file ${latestFile} is missing output data.`);
114
- }
115
- const filteredAstData = { ...buildOutput };
116
- if (filteredAstData.sources) {
117
- const validSources = {};
118
- for (const [key, content] of Object.entries(filteredAstData.sources)) {
119
- const ast = content.ast || content.legacyAST || content.AST;
120
- if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
121
- validSources[key] = content;
122
- }
123
- }
124
- filteredAstData.sources = validSources;
125
- }
126
- const reader = new solc_typed_ast_1.ASTReader();
127
- return reader.read(filteredAstData);
128
- };
129
88
  const shouldIncludeFunction = (fnDef) => {
130
89
  if (!fnDef.implemented)
131
90
  return false;
@@ -204,7 +163,7 @@ const runCoverage = async (foundryRoot, foundryConfigPath, cryticName, suiteName
204
163
  const outDir = path.join(foundryRoot, '.recon', 'out');
205
164
  const buildCmd = `forge build --contracts ${cryticName} --build-info --out .recon/out`.replace(/\s+/g, ' ').trim();
206
165
  await runCmd(buildCmd, foundryRoot);
207
- const sourceUnits = await loadLatestSourceUnits(foundryRoot);
166
+ const { sourceUnits } = await (0, utils_1.loadLatestBuildInfo)(foundryRoot, '.recon/out');
208
167
  if (!sourceUnits || sourceUnits.length === 0) {
209
168
  throw new Error('No source units were produced from the Crytic build; cannot generate coverage.');
210
169
  }
@@ -17,7 +17,7 @@ interface DictionaryOutput {
17
17
  /**
18
18
  * Run the dictionary command
19
19
  */
20
- export declare const runDictionary: (buildInfoPath: string, options?: {
20
+ export declare const runDictionary: (foundryRoot: string, options?: {
21
21
  outputPath?: string;
22
22
  json?: boolean;
23
23
  srcPrefix?: string;
@@ -37,32 +37,6 @@ exports.runDictionary = void 0;
37
37
  const fs = __importStar(require("fs/promises"));
38
38
  const solc_typed_ast_1 = require("solc-typed-ast");
39
39
  const utils_1 = require("./utils");
40
- /**
41
- * Load build-info from a JSON file
42
- */
43
- const loadBuildInfo = async (buildInfoPath) => {
44
- var _a;
45
- const fileContent = await fs.readFile(buildInfoPath, 'utf-8');
46
- const buildInfo = JSON.parse(fileContent);
47
- const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
48
- if (!buildOutput) {
49
- throw new Error(`Build-info file ${buildInfoPath} is missing output data.`);
50
- }
51
- const filteredAstData = { ...buildOutput };
52
- if (filteredAstData.sources) {
53
- const validSources = {};
54
- for (const [key, content] of Object.entries(filteredAstData.sources)) {
55
- const ast = content.ast || content.legacyAST || content.AST;
56
- if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
57
- validSources[key] = content;
58
- }
59
- }
60
- filteredAstData.sources = validSources;
61
- }
62
- const reader = new solc_typed_ast_1.ASTReader();
63
- const sourceUnits = reader.read(filteredAstData);
64
- return { sourceUnits };
65
- };
66
40
  /**
67
41
  * Check if a type string represents an address, contract, or interface type
68
42
  */
@@ -181,14 +155,14 @@ const findStateVarCasts = (contract, stateVarMap) => {
181
155
  /**
182
156
  * Run the dictionary command
183
157
  */
184
- const runDictionary = async (buildInfoPath, options = {}) => {
158
+ const runDictionary = async (foundryRoot, options = {}) => {
185
159
  var _a;
186
160
  const log = options.json ? () => { } : console.log.bind(console);
187
161
  const srcPrefix = (_a = options.srcPrefix) !== null && _a !== void 0 ? _a : 'src/';
188
- log(`[recon-generate] Loading build-info from ${buildInfoPath}`);
189
- const { sourceUnits } = await loadBuildInfo(buildInfoPath);
162
+ log(`[recon-generate] Loading build-info from ${foundryRoot}`);
163
+ const { sourceUnits } = await (0, utils_1.loadLatestBuildInfo)(foundryRoot);
190
164
  if (!sourceUnits || sourceUnits.length === 0) {
191
- throw new Error('No source units were produced from the build-info; cannot generate dictionary.');
165
+ throw new Error('No source units were produced from the build; cannot generate dictionary.');
192
166
  }
193
167
  log(`[recon-generate] Found ${sourceUnits.length} source units`);
194
168
  const output = {};
package/dist/generator.js CHANGED
@@ -46,7 +46,6 @@ const templateManager_1 = require("./templateManager");
46
46
  const processor_1 = require("./processor");
47
47
  const types_1 = require("./types");
48
48
  const utils_1 = require("./utils");
49
- const solc_typed_ast_1 = require("solc-typed-ast");
50
49
  const inputParams_1 = require("./inputParams");
51
50
  class ReconGenerator {
52
51
  constructor(foundryRoot, options) {
@@ -546,7 +545,7 @@ class ReconGenerator {
546
545
  return config;
547
546
  }
548
547
  async run() {
549
- var _a, _b, _c, _d, _e;
548
+ var _a, _b, _c;
550
549
  await this.ensureFoundryConfigExists();
551
550
  const outPath = this.outDir();
552
551
  const mockTargets = (_a = this.options.mocks) !== null && _a !== void 0 ? _a : new Set();
@@ -568,71 +567,24 @@ class ReconGenerator {
568
567
  await this.ensureBuild(); // first build to get ABIs
569
568
  }
570
569
  let sourceUnits = [];
571
- const reader = new solc_typed_ast_1.ASTReader();
572
570
  try {
573
- const buildInfoDir = path.join(this.outDir(), 'build-info');
574
- this.logDebug('Scanning build-info directory', { buildInfoDir });
575
- const allFiles = await fs.readdir(buildInfoDir);
576
- const jsonFiles = allFiles.filter(f => f.endsWith('.json'));
577
- const filesWithStats = await Promise.all(jsonFiles.map(async (f) => ({
578
- name: f,
579
- path: path.join(buildInfoDir, f),
580
- mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime
581
- })));
582
- const files = filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
583
- if (files.length === 0) {
584
- console.warn('❌ No build-info JSON files found');
585
- }
586
- const latestFile = (_b = files[0]) === null || _b === void 0 ? void 0 : _b.path;
587
- if (latestFile) {
588
- const fileContent = await fs.readFile(latestFile, 'utf-8');
589
- const buildInfo = JSON.parse(fileContent);
590
- const buildOutput = (_c = buildInfo.output) !== null && _c !== void 0 ? _c : buildInfo;
591
- if (buildOutput) {
592
- const filteredAstData = { ...buildOutput };
593
- if (filteredAstData.sources) {
594
- const validSources = {};
595
- // Paths to skip - test and script directories
596
- const skipPatterns = [
597
- 'test/',
598
- 'tests/',
599
- 'script/',
600
- 'scripts/',
601
- 'contracts/test/',
602
- 'contracts/script/',
603
- 'contracts/scripts/',
604
- 'forge-std/'
605
- ];
606
- for (const [key, content] of Object.entries(filteredAstData.sources)) {
607
- // Skip test and script files
608
- const shouldSkip = skipPatterns.some(pattern => key.includes(pattern));
609
- if (shouldSkip) {
610
- this.logDebug('Skipping source (pattern match)', { key });
611
- continue;
612
- }
613
- const ast = content.ast || content.legacyAST || content.AST;
614
- if (ast && (ast.nodeType === "SourceUnit" || ast.name === "SourceUnit")) {
615
- validSources[key] = content;
616
- }
617
- else {
618
- this.logDebug('Skipping source (no source unit)', { key });
619
- }
620
- }
621
- filteredAstData.sources = validSources;
622
- this.logDebug('Filtered sources for AST read', { kept: Object.keys(validSources).length, keys: Object.keys(validSources) });
623
- }
624
- else {
625
- this.logDebug('No sources present in build output');
626
- }
627
- try {
628
- sourceUnits = reader.read(filteredAstData);
629
- }
630
- catch (e) {
631
- }
632
- }
633
- }
571
+ // Paths to skip - test and script directories
572
+ const skipPatterns = [
573
+ 'test/',
574
+ 'tests/',
575
+ 'script/',
576
+ 'scripts/',
577
+ 'contracts/test/',
578
+ 'contracts/script/',
579
+ 'contracts/scripts/',
580
+ 'forge-std/'
581
+ ];
582
+ const result = await (0, utils_1.loadLatestBuildInfo)(this.foundryRoot, '.recon/out', skipPatterns);
583
+ sourceUnits = result.sourceUnits;
584
+ this.logDebug('Loaded source units', { count: sourceUnits.length });
634
585
  }
635
586
  catch (e) {
587
+ this.logDebug('Failed to load build info', { error: String(e) });
636
588
  }
637
589
  // Generate mocks if requested, then rebuild to pick them up
638
590
  let tempMockSrc = null;
@@ -687,7 +639,7 @@ class ReconGenerator {
687
639
  continue;
688
640
  }
689
641
  const fnSet = new Set();
690
- for (const fn of (_d = cfg.functions) !== null && _d !== void 0 ? _d : []) {
642
+ for (const fn of (_b = cfg.functions) !== null && _b !== void 0 ? _b : []) {
691
643
  if (fn === null || fn === void 0 ? void 0 : fn.signature) {
692
644
  fnSet.add(String(fn.signature));
693
645
  }
@@ -723,7 +675,7 @@ class ReconGenerator {
723
675
  this.logDebug('Wrote parameter file', { paramPath });
724
676
  }
725
677
  }
726
- const tm = new templateManager_1.TemplateManager(this.foundryRoot, this.options.suiteDir, this.options.suiteNameSnake, this.options.suiteNamePascal, (_e = this.options.dynamicDeploy) !== null && _e !== void 0 ? _e : new Set());
678
+ const tm = new templateManager_1.TemplateManager(this.foundryRoot, this.options.suiteDir, this.options.suiteNameSnake, this.options.suiteNamePascal, (_c = this.options.dynamicDeploy) !== null && _c !== void 0 ? _c : new Set());
727
679
  await tm.generateTemplates(collected.enabledContracts, collected.adminFunctions, collected.nonSeparatedFunctions, collected.separatedByContract, collected.allContractNames);
728
680
  }
729
681
  async listAvailable() {
package/dist/index.js CHANGED
@@ -47,6 +47,8 @@ const link_1 = require("./link");
47
47
  const link2_1 = require("./link2");
48
48
  const sourcemap_1 = require("./sourcemap");
49
49
  const dictionary_1 = require("./dictionary");
50
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
51
+ const packageJson = require('../package.json');
50
52
  function parseFilter(input) {
51
53
  if (!input)
52
54
  return undefined;
@@ -114,6 +116,7 @@ async function main() {
114
116
  const program = new commander_1.Command();
115
117
  program
116
118
  .name('recon-generate')
119
+ .version(packageJson.version, '-v, --version', 'Output the current version')
117
120
  .description('Scaffold Recon fuzzing suite inside a Foundry project')
118
121
  .option('-o, --output <path>', 'Custom output folder for tests (recon will be appended if missing)')
119
122
  .option('-r, --recon <path>', 'Path to recon.json configuration')
@@ -245,21 +248,22 @@ async function main() {
245
248
  });
246
249
  });
247
250
  program
248
- .command('dictionary <buildInfo>')
251
+ .command('dictionary')
249
252
  .description('Extract storage variables with contract/interface types from build-info')
250
253
  .option('-o, --output <path>', 'Custom output path for the dictionary JSON file')
251
254
  .option('--json', 'Output JSON to terminal (also saves to file if -o is specified)')
252
255
  .option('--src-prefix <prefix>', 'Source directory prefix to filter (default: "src/")', 'src/')
253
- .action(async function (buildInfoPath) {
256
+ .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
257
+ .action(async function () {
254
258
  // @ts-ignore - Commander types are complex
255
259
  const opts = this.opts();
256
- const resolvedBuildInfo = path.isAbsolute(buildInfoPath)
257
- ? buildInfoPath
258
- : path.resolve(process.cwd(), buildInfoPath);
260
+ const workspaceRoot = process.cwd();
261
+ const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
262
+ const foundryRoot = path.dirname(foundryConfig);
259
263
  const outputPath = opts.output
260
- ? (path.isAbsolute(opts.output) ? opts.output : path.resolve(process.cwd(), opts.output))
264
+ ? (path.isAbsolute(opts.output) ? opts.output : path.join(foundryRoot, opts.output))
261
265
  : undefined;
262
- await (0, dictionary_1.runDictionary)(resolvedBuildInfo, {
266
+ await (0, dictionary_1.runDictionary)(foundryRoot, {
263
267
  outputPath,
264
268
  json: !!opts.json,
265
269
  srcPrefix: opts.srcPrefix,
package/dist/info.d.ts CHANGED
@@ -32,6 +32,7 @@ interface InfoOutput {
32
32
  with_fallback: string[];
33
33
  with_receive: string[];
34
34
  coverage_map: Record<string, Record<string, CoverageBlock>>;
35
+ exclude_from_fuzzing: string[];
35
36
  }
36
37
  export declare const runInfo: (foundryRoot: string, contractName: string, options?: {
37
38
  outputPath?: string;
package/dist/info.js CHANGED
@@ -40,34 +40,10 @@ const solc_typed_ast_1 = require("solc-typed-ast");
40
40
  const utils_1 = require("./utils");
41
41
  const z3Solver_1 = require("./z3Solver");
42
42
  const call_tree_builder_1 = require("./analyzer/call-tree-builder");
43
- const loadLatestBuildInfo = async (foundryRoot) => {
44
- var _a;
45
- const outDir = path.join(foundryRoot, 'out');
46
- const buildInfoDir = path.join(outDir, 'build-info');
47
- let files = [];
48
- try {
49
- const entries = await fs.readdir(buildInfoDir);
50
- const jsonFiles = entries.filter((f) => f.endsWith('.json'));
51
- files = await Promise.all(jsonFiles.map(async (f) => ({
52
- name: f,
53
- path: path.join(buildInfoDir, f),
54
- mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime,
55
- })));
56
- }
57
- catch (e) {
58
- throw new Error(`No build-info directory found at ${buildInfoDir}: ${e}`);
59
- }
60
- if (files.length === 0) {
61
- throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
62
- }
63
- const latestFile = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path;
64
- const fileContent = await fs.readFile(latestFile, 'utf-8');
65
- const buildInfo = JSON.parse(fileContent);
66
- const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
67
- if (!buildOutput) {
68
- throw new Error(`Build-info file ${latestFile} is missing output data.`);
69
- }
70
- // Extract ABIs from contracts
43
+ /**
44
+ * Extract ABIs from build output
45
+ */
46
+ const extractAbis = (buildOutput) => {
71
47
  const contractAbis = new Map();
72
48
  if (buildOutput.contracts) {
73
49
  for (const [_filePath, contracts] of Object.entries(buildOutput.contracts)) {
@@ -78,20 +54,7 @@ const loadLatestBuildInfo = async (foundryRoot) => {
78
54
  }
79
55
  }
80
56
  }
81
- const filteredAstData = { ...buildOutput };
82
- if (filteredAstData.sources) {
83
- const validSources = {};
84
- for (const [key, content] of Object.entries(filteredAstData.sources)) {
85
- const ast = content.ast || content.legacyAST || content.AST;
86
- if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
87
- validSources[key] = content;
88
- }
89
- }
90
- filteredAstData.sources = validSources;
91
- }
92
- const reader = new solc_typed_ast_1.ASTReader();
93
- const sourceUnits = reader.read(filteredAstData);
94
- return { sourceUnits, contractAbis };
57
+ return contractAbis;
95
58
  };
96
59
  const stripDataLocation = (raw) => {
97
60
  if (!raw)
@@ -907,6 +870,39 @@ const hasReceive = (contract) => {
907
870
  const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true);
908
871
  return allFunctions.some(fn => fn.kind === solc_typed_ast_1.FunctionKind.Receive);
909
872
  };
873
+ /**
874
+ * Check if a function contains a fuzzFromHere() call
875
+ */
876
+ const hasFuzzFromHereCall = (fnDef) => {
877
+ for (const call of fnDef.getChildrenByType(solc_typed_ast_1.FunctionCall)) {
878
+ const expr = call.vExpression;
879
+ if (expr instanceof solc_typed_ast_1.MemberAccess && expr.memberName === 'fuzzFromHere') {
880
+ return true;
881
+ }
882
+ }
883
+ return false;
884
+ };
885
+ /**
886
+ * Collect public/external functions that contain vm.fuzzFromHere() calls
887
+ * These functions should be excluded from fuzzing
888
+ */
889
+ const collectExcludeFromFuzzing = (contract) => {
890
+ const exclude = [];
891
+ const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
892
+ for (const fnDef of allFunctions) {
893
+ // Only check public/external functions
894
+ if (fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.External && fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.Public) {
895
+ continue;
896
+ }
897
+ if (hasFuzzFromHereCall(fnDef)) {
898
+ // Use function name (not signature) as per the expected output format
899
+ if (!exclude.includes(fnDef.name)) {
900
+ exclude.push(fnDef.name);
901
+ }
902
+ }
903
+ }
904
+ return exclude;
905
+ };
910
906
  /**
911
907
  * Load source file contents for source location info
912
908
  */
@@ -932,10 +928,11 @@ const runInfo = async (foundryRoot, contractName, options = {}) => {
932
928
  if (options.json) {
933
929
  (0, z3Solver_1.setZ3Silent)(true);
934
930
  }
935
- const { sourceUnits, contractAbis } = await loadLatestBuildInfo(foundryRoot);
931
+ const { sourceUnits, buildOutput } = await (0, utils_1.loadLatestBuildInfo)(foundryRoot);
936
932
  if (!sourceUnits || sourceUnits.length === 0) {
937
933
  throw new Error('No source units were produced from the build; cannot generate info.');
938
934
  }
935
+ const contractAbis = extractAbis(buildOutput);
939
936
  // Load source file contents for assert info
940
937
  const sourceContents = await loadSourceContents(sourceUnits);
941
938
  const allContracts = getAllContracts(sourceUnits);
@@ -956,7 +953,10 @@ const runInfo = async (foundryRoot, contractName, options = {}) => {
956
953
  with_fallback: [],
957
954
  with_receive: [],
958
955
  coverage_map: {},
956
+ exclude_from_fuzzing: [],
959
957
  };
958
+ // Collect functions to exclude from fuzzing (from target contract)
959
+ output.exclude_from_fuzzing = collectExcludeFromFuzzing(targetContract);
960
960
  // Collect info for all related contracts
961
961
  for (const contract of relatedContracts) {
962
962
  // Skip abstract contracts (their functions are inherited by concrete contracts)
@@ -61,52 +61,11 @@ const runCmd = (cmd, cwd) => {
61
61
  });
62
62
  });
63
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
64
  // ==================== Main Entry ====================
106
65
  const runPaths = async (foundryRoot, cryticName, suiteNameSnake, options = {}) => {
107
66
  const buildCmd = `forge build --contracts ${cryticName} --build-info --out .recon/out`.replace(/\s+/g, ' ').trim();
108
67
  await runCmd(buildCmd, foundryRoot);
109
- const sourceUnits = await loadLatestSourceUnits(foundryRoot);
68
+ const { sourceUnits } = await (0, utils_1.loadLatestBuildInfo)(foundryRoot, '.recon/out');
110
69
  if (!sourceUnits || sourceUnits.length === 0) {
111
70
  throw new Error('No source units were produced from the Crytic build; cannot generate paths.');
112
71
  }
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ASTNode, ContractDefinition, Assignment, FunctionCall, FunctionDefinition, VariableDeclaration, EventDefinition, ErrorDefinition } from 'solc-typed-ast';
1
+ import { ASTNode, ContractDefinition, Assignment, FunctionCall, FunctionDefinition, SourceUnit, VariableDeclaration, EventDefinition, ErrorDefinition } from 'solc-typed-ast';
2
2
  import { CallType } from './types';
3
3
  export declare function fileExists(p: string): Promise<boolean>;
4
4
  export declare function getFoundryConfigPath(workspaceRoot: string, override?: string): string;
@@ -39,3 +39,36 @@ export declare function forwardLinearization(root: ContractDefinition): Contract
39
39
  * contract. `current` is the contract whose code contains the `super` call.
40
40
  */
41
41
  export declare function resolveSuper(root: ContractDefinition, current: ContractDefinition, signature: string): FunctionDefinition | undefined;
42
+ /**
43
+ * Result from loading build info
44
+ */
45
+ export interface BuildInfoResult {
46
+ sourceUnits: SourceUnit[];
47
+ buildOutput: any;
48
+ }
49
+ /**
50
+ * Parse build output and return source units.
51
+ * This is the core parsing logic used by both loadLatestBuildInfo and loadBuildInfoFromFile.
52
+ *
53
+ * @param buildOutput - The raw build output object
54
+ * @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
55
+ * @returns Array of SourceUnit objects
56
+ */
57
+ export declare const parseBuildOutput: (buildOutput: any, skipPatterns?: string[]) => SourceUnit[];
58
+ /**
59
+ * Load build info from a specific file path.
60
+ *
61
+ * @param buildInfoPath - Path to the build-info JSON file
62
+ * @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
63
+ * @returns Object containing sourceUnits and raw buildOutput
64
+ */
65
+ export declare const loadBuildInfoFromFile: (buildInfoPath: string, skipPatterns?: string[]) => Promise<BuildInfoResult>;
66
+ /**
67
+ * Load the latest build info from a Foundry project.
68
+ *
69
+ * @param foundryRoot - The root directory of the Foundry project
70
+ * @param outputDir - The output directory relative to foundryRoot (default: 'out')
71
+ * @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
72
+ * @returns Object containing sourceUnits and raw buildOutput
73
+ */
74
+ export declare const loadLatestBuildInfo: (foundryRoot: string, outputDir?: string, skipPatterns?: string[]) => Promise<BuildInfoResult>;
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.signatureEquals = exports.getFunctionName = exports.getIndex = exports.getSource = exports.getLines = void 0;
39
+ exports.loadLatestBuildInfo = exports.loadBuildInfoFromFile = exports.parseBuildOutput = exports.signatureEquals = exports.getFunctionName = exports.getIndex = exports.getSource = exports.getLines = void 0;
40
40
  exports.fileExists = fileExists;
41
41
  exports.getFoundryConfigPath = getFoundryConfigPath;
42
42
  exports.findOutputDirectory = findOutputDirectory;
@@ -436,3 +436,84 @@ function resolveSuper(root, current, signature) {
436
436
  }
437
437
  return undefined;
438
438
  }
439
+ /**
440
+ * Parse build output and return source units.
441
+ * This is the core parsing logic used by both loadLatestBuildInfo and loadBuildInfoFromFile.
442
+ *
443
+ * @param buildOutput - The raw build output object
444
+ * @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
445
+ * @returns Array of SourceUnit objects
446
+ */
447
+ const parseBuildOutput = (buildOutput, skipPatterns = []) => {
448
+ const filteredAstData = { ...buildOutput };
449
+ if (filteredAstData.sources) {
450
+ const validSources = {};
451
+ for (const [key, content] of Object.entries(filteredAstData.sources)) {
452
+ // Skip sources matching any of the skip patterns
453
+ if (skipPatterns.length > 0) {
454
+ const shouldSkip = skipPatterns.some(pattern => key.includes(pattern));
455
+ if (shouldSkip) {
456
+ continue;
457
+ }
458
+ }
459
+ const ast = content.ast || content.legacyAST || content.AST;
460
+ if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
461
+ validSources[key] = content;
462
+ }
463
+ }
464
+ filteredAstData.sources = validSources;
465
+ }
466
+ const reader = new solc_typed_ast_1.ASTReader();
467
+ return reader.read(filteredAstData);
468
+ };
469
+ exports.parseBuildOutput = parseBuildOutput;
470
+ /**
471
+ * Load build info from a specific file path.
472
+ *
473
+ * @param buildInfoPath - Path to the build-info JSON file
474
+ * @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
475
+ * @returns Object containing sourceUnits and raw buildOutput
476
+ */
477
+ const loadBuildInfoFromFile = async (buildInfoPath, skipPatterns = []) => {
478
+ var _a;
479
+ const fileContent = await fs.readFile(buildInfoPath, 'utf-8');
480
+ const buildInfo = JSON.parse(fileContent);
481
+ const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
482
+ if (!buildOutput) {
483
+ throw new Error(`Build-info file ${buildInfoPath} is missing output data.`);
484
+ }
485
+ const sourceUnits = (0, exports.parseBuildOutput)(buildOutput, skipPatterns);
486
+ return { sourceUnits, buildOutput };
487
+ };
488
+ exports.loadBuildInfoFromFile = loadBuildInfoFromFile;
489
+ /**
490
+ * Load the latest build info from a Foundry project.
491
+ *
492
+ * @param foundryRoot - The root directory of the Foundry project
493
+ * @param outputDir - The output directory relative to foundryRoot (default: 'out')
494
+ * @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
495
+ * @returns Object containing sourceUnits and raw buildOutput
496
+ */
497
+ const loadLatestBuildInfo = async (foundryRoot, outputDir = 'out', skipPatterns = []) => {
498
+ const outDir = path.join(foundryRoot, outputDir);
499
+ const buildInfoDir = path.join(outDir, 'build-info');
500
+ let files = [];
501
+ try {
502
+ const entries = await fs.readdir(buildInfoDir);
503
+ const jsonFiles = entries.filter((f) => f.endsWith('.json'));
504
+ files = await Promise.all(jsonFiles.map(async (f) => ({
505
+ name: f,
506
+ path: path.join(buildInfoDir, f),
507
+ mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime,
508
+ })));
509
+ }
510
+ catch (e) {
511
+ throw new Error(`No build-info directory found at ${buildInfoDir}: ${e}`);
512
+ }
513
+ if (files.length === 0) {
514
+ throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
515
+ }
516
+ const latestFile = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path;
517
+ return (0, exports.loadBuildInfoFromFile)(latestFile, skipPatterns);
518
+ };
519
+ exports.loadLatestBuildInfo = loadLatestBuildInfo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recon-generate",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {