recon-generate 0.0.31 → 0.0.33

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.
@@ -1,3 +1,4 @@
1
+ import { FoundryConfig } from './types';
1
2
  export type FilterSpec = {
2
3
  contractOnly: Set<string>;
3
4
  functions: Map<string, Set<string>>;
@@ -18,6 +19,7 @@ export interface GeneratorOptions {
18
19
  suiteNamePascal: string;
19
20
  coverage?: boolean;
20
21
  params?: boolean;
22
+ foundryConfig?: FoundryConfig;
21
23
  }
22
24
  export declare class ReconGenerator {
23
25
  private foundryRoot;
@@ -26,7 +28,9 @@ export declare class ReconGenerator {
26
28
  private allowedMockNames;
27
29
  private contractKindCache;
28
30
  private abstractContractCache;
31
+ private _foundryConfig?;
29
32
  constructor(foundryRoot: string, options: GeneratorOptions);
33
+ private getConfig;
30
34
  private logDebug;
31
35
  private outDir;
32
36
  private ensureReconFolder;
package/dist/generator.js CHANGED
@@ -55,6 +55,16 @@ class ReconGenerator {
55
55
  this.allowedMockNames = new Set();
56
56
  this.contractKindCache = new Map();
57
57
  this.abstractContractCache = new Map();
58
+ if (options.foundryConfig) {
59
+ this._foundryConfig = options.foundryConfig;
60
+ }
61
+ }
62
+ async getConfig() {
63
+ if (!this._foundryConfig) {
64
+ this._foundryConfig = await (0, utils_1.getFoundryConfig)(this.foundryRoot);
65
+ this.logDebug('Resolved foundry config', this._foundryConfig);
66
+ }
67
+ return this._foundryConfig;
58
68
  }
59
69
  logDebug(message, obj) {
60
70
  if (!this.options.debug)
@@ -103,7 +113,10 @@ class ReconGenerator {
103
113
  return;
104
114
  }
105
115
  await this.ensureReconFolder();
106
- const skipArg = '--skip */test/** */tests/** */script/** */scripts/** */contracts/test/**';
116
+ const config = await this.getConfig();
117
+ const skipDirs = new Set([config.test, config.script]);
118
+ const skipGlobs = Array.from(skipDirs).map(d => `*/${d}/**`);
119
+ const skipArg = skipGlobs.length > 0 ? `--skip ${skipGlobs.join(' ')}` : '';
107
120
  const cmd = `forge build --build-info ${skipArg} --out .recon/out`.replace(/\s+/g, ' ').trim();
108
121
  await this.runCmd(cmd, this.foundryRoot);
109
122
  }
@@ -122,6 +135,14 @@ class ReconGenerator {
122
135
  await this.runCmd('forge install Recon-Fuzz/setup-helpers', this.foundryRoot);
123
136
  }
124
137
  async updateRemappings() {
138
+ const config = await this.getConfig();
139
+ // If foundry config already has @chimera and @recon remappings, skip file manipulation
140
+ const hasChimera = config.remappings.some(r => r.startsWith('@chimera'));
141
+ const hasRecon = config.remappings.some(r => r.startsWith('@recon'));
142
+ if (hasChimera && hasRecon) {
143
+ this.logDebug('Remappings for @chimera and @recon already present in foundry config, skipping remappings.txt update');
144
+ return;
145
+ }
125
146
  const remappingsPath = path.join(this.foundryRoot, 'remappings.txt');
126
147
  if (!(await (0, utils_1.fileExists)(remappingsPath))) {
127
148
  await this.runCmd('forge remappings > remappings.txt', this.foundryRoot);
@@ -133,8 +154,10 @@ class ReconGenerator {
133
154
  .split('\n')
134
155
  .map(line => line.trim())
135
156
  .filter(line => line && !line.startsWith('@chimera') && !line.startsWith('@recon'));
136
- remappings.push(chimeraMapping);
137
- remappings.push(setupToolsMapping);
157
+ if (!hasChimera)
158
+ remappings.push(chimeraMapping);
159
+ if (!hasRecon)
160
+ remappings.push(setupToolsMapping);
138
161
  await fs.writeFile(remappingsPath, remappings.join('\n'));
139
162
  }
140
163
  async updateGitignore() {
@@ -248,6 +271,7 @@ class ReconGenerator {
248
271
  var _a, _b;
249
272
  const contracts = [];
250
273
  const outDir = this.outDir();
274
+ const config = await this.getConfig();
251
275
  try {
252
276
  if (!(await (0, utils_1.fileExists)(outDir))) {
253
277
  throw new Error(`Compiled output not found at ${outDir}. Please ensure forge build runs successfully (we attempt it automatically).`);
@@ -280,13 +304,11 @@ class ReconGenerator {
280
304
  : path.join(this.foundryRoot, sourcePath);
281
305
  const relSourcePath = path.relative(this.foundryRoot, absSourcePath).replace(/\\/g, '/').toLowerCase();
282
306
  const skipPrefixes = [
283
- 'test/',
284
- 'tests/',
285
- 'script/',
286
- 'scripts/',
287
- 'contracts/test/',
288
- 'contracts/tests/',
289
- 'lib/',
307
+ ...new Set([
308
+ `${config.test}/`,
309
+ `${config.script}/`,
310
+ ...config.libs.map(l => `${l}/`),
311
+ ]),
290
312
  ];
291
313
  // Check if contract is explicitly included or mocked - if so, don't filter by kind
292
314
  const isExplicitlyIncluded = this.options.include &&
@@ -450,7 +472,8 @@ class ReconGenerator {
450
472
  if (!this.options.mocks || this.options.mocks.size === 0) {
451
473
  return null;
452
474
  }
453
- const tempSrc = path.join(this.foundryRoot, 'src', '__recon');
475
+ const config = await this.getConfig();
476
+ const tempSrc = path.join(this.foundryRoot, config.src, '__recon');
454
477
  await fs.mkdir(tempSrc, { recursive: true });
455
478
  for (const name of this.options.mocks) {
456
479
  const mockName = this.mockNameFor(name);
@@ -566,22 +589,21 @@ class ReconGenerator {
566
589
  }
567
590
  await this.ensureBuild(); // first build to get ABIs
568
591
  }
592
+ const config = await this.getConfig();
569
593
  let sourceUnits = [];
594
+ let symbolsInfo = { byFile: new Map(), definedIn: new Map() };
570
595
  try {
571
- // Paths to skip - test and script directories
572
596
  const skipPatterns = [
573
- 'test/',
574
- 'tests/',
575
- 'script/',
576
- 'scripts/',
577
- 'contracts/test/',
578
- 'contracts/script/',
579
- 'contracts/scripts/',
580
- 'forge-std/'
597
+ `${config.test}/`,
598
+ `${config.script}/`,
599
+ ...config.libs.map(l => `${l}/`),
600
+ 'forge-std/',
581
601
  ];
582
602
  const result = await (0, utils_1.loadLatestBuildInfo)(this.foundryRoot, '.recon/out', skipPatterns);
583
603
  sourceUnits = result.sourceUnits;
604
+ symbolsInfo = (0, utils_1.buildExportedSymbolsMap)(result.buildOutput);
584
605
  this.logDebug('Loaded source units', { count: sourceUnits.length });
606
+ this.logDebug('Built symbol maps', { files: symbolsInfo.byFile.size, symbols: symbolsInfo.definedIn.size });
585
607
  }
586
608
  catch (e) {
587
609
  this.logDebug('Failed to load build info', { error: String(e) });
@@ -676,7 +698,7 @@ class ReconGenerator {
676
698
  }
677
699
  }
678
700
  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());
679
- await tm.generateTemplates(collected.enabledContracts, collected.adminFunctions, collected.nonSeparatedFunctions, collected.separatedByContract, collected.allContractNames);
701
+ await tm.generateTemplates(collected.enabledContracts, collected.adminFunctions, collected.nonSeparatedFunctions, collected.separatedByContract, collected.allContractNames, symbolsInfo);
680
702
  }
681
703
  async listAvailable() {
682
704
  var _a;
package/dist/index.js CHANGED
@@ -336,8 +336,9 @@ async function main() {
336
336
  program
337
337
  .action(async (opts) => {
338
338
  const workspaceRoot = process.cwd();
339
- const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
340
- const foundryRoot = path.dirname(foundryConfig);
339
+ const foundryConfigPath = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
340
+ const foundryRoot = path.dirname(foundryConfigPath);
341
+ const forgeConfig = await (0, utils_1.getFoundryConfig)(foundryRoot);
341
342
  const includeFilter = parseFilter(opts.include);
342
343
  const excludeFilter = parseFilter(opts.exclude);
343
344
  const adminFilter = parseFilter(opts.admin);
@@ -356,7 +357,7 @@ async function main() {
356
357
  const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
357
358
  const suitePascal = suiteRaw ? (0, case_1.pascal)(suiteRaw) : '';
358
359
  const suiteFolderName = suiteSnake ? `recon-${suiteSnake}` : 'recon';
359
- const baseOut = opts.output ? String(opts.output) : await (0, utils_1.getTestFolder)(foundryRoot);
360
+ const baseOut = opts.output ? String(opts.output) : forgeConfig.test;
360
361
  const baseOutPath = path.isAbsolute(baseOut) ? baseOut : path.join(foundryRoot, baseOut);
361
362
  const suiteDir = path.join(baseOutPath, suiteFolderName);
362
363
  const reconDefaultName = suiteSnake ? `recon-${suiteSnake}.json` : 'recon.json';
@@ -368,7 +369,7 @@ async function main() {
368
369
  const generator = new generator_1.ReconGenerator(foundryRoot, {
369
370
  suiteDir,
370
371
  reconConfigPath: reconPath,
371
- foundryConfigPath: foundryConfig,
372
+ foundryConfigPath: foundryConfigPath,
372
373
  include: includeFilter,
373
374
  exclude: excludeFilter,
374
375
  admin: adminFilter ? adminFilter.functions : undefined,
@@ -381,6 +382,7 @@ async function main() {
381
382
  suiteNamePascal: suitePascal,
382
383
  coverage: !!opts.coverage,
383
384
  params: !!opts.params,
385
+ foundryConfig: forgeConfig,
384
386
  });
385
387
  if (opts.list) {
386
388
  await generator.listAvailable();
@@ -1,4 +1,5 @@
1
1
  import { FunctionDefinitionParams, ContractMetadata } from './types';
2
+ import { SymbolsInfo } from './utils';
2
3
  export declare class TemplateManager {
3
4
  private foundryRoot;
4
5
  private suiteDir;
@@ -9,5 +10,5 @@ export declare class TemplateManager {
9
10
  private skipList;
10
11
  private shouldGenerateFile;
11
12
  private updateTargetFile;
12
- generateTemplates(enabledContracts: ContractMetadata[], adminFunctions: FunctionDefinitionParams[], nonSeparatedFunctions: FunctionDefinitionParams[], separatedByContract: Record<string, FunctionDefinitionParams[]>, allContractNames: string[]): Promise<void>;
13
+ generateTemplates(enabledContracts: ContractMetadata[], adminFunctions: FunctionDefinitionParams[], nonSeparatedFunctions: FunctionDefinitionParams[], separatedByContract: Record<string, FunctionDefinitionParams[]>, allContractNames: string[], symbolsInfo?: SymbolsInfo): Promise<void>;
13
14
  }
@@ -38,6 +38,7 @@ const path = __importStar(require("path"));
38
38
  const fs = __importStar(require("fs/promises"));
39
39
  const case_1 = require("case");
40
40
  const templates = __importStar(require("./templates"));
41
+ const utils_1 = require("./utils");
41
42
  class TemplateManager {
42
43
  constructor(foundryRoot, suiteDir, suiteNameSnake, suiteNamePascal, dynamicDeploy = new Set()) {
43
44
  this.foundryRoot = foundryRoot;
@@ -96,7 +97,7 @@ class TemplateManager {
96
97
  return newContent;
97
98
  }
98
99
  }
99
- async generateTemplates(enabledContracts, adminFunctions, nonSeparatedFunctions, separatedByContract, allContractNames) {
100
+ async generateTemplates(enabledContracts, adminFunctions, nonSeparatedFunctions, separatedByContract, allContractNames, symbolsInfo) {
100
101
  const contractsForSetup = enabledContracts.map(contract => ({ ...contract, isDynamic: this.dynamicDeploy.has(contract.name) }));
101
102
  const dynamicContracts = enabledContracts
102
103
  .filter(contract => this.dynamicDeploy.has(contract.name))
@@ -120,16 +121,22 @@ class TemplateManager {
120
121
  contracts: allContractNames,
121
122
  dynamicContracts,
122
123
  }),
123
- [path.join(suiteTargetsDir, 'AdminTargets.sol')]: templates.adminTargetsTemplate({ functions: adminFunctions }),
124
+ [path.join(suiteTargetsDir, 'AdminTargets.sol')]: templates.adminTargetsTemplate({
125
+ functions: adminFunctions,
126
+ extraImports: symbolsInfo ? (0, utils_1.resolveExtraImports)([], adminFunctions, symbolsInfo) : [],
127
+ }),
124
128
  [path.join(suiteTargetsDir, 'DoomsdayTargets.sol')]: templates.doomsdayTargetsTemplate({}),
125
129
  [path.join(suiteTargetsDir, 'ManagersTargets.sol')]: templates.managersTargetsTemplate({}),
126
130
  };
127
131
  for (const [contractName, functions] of Object.entries(separatedByContract)) {
128
132
  const targetPath = path.join(suiteTargetsDir, `${(0, case_1.pascal)(contractName)}Targets.sol`);
133
+ const contractPath = functions.length > 0 ? functions[0].contractPath : '';
134
+ const importedPaths = contractPath ? [contractPath] : [];
129
135
  files[targetPath] = templates.targetsTemplate({
130
136
  contractName,
131
- path: functions.length > 0 ? functions[0].contractPath : '',
137
+ path: contractPath,
132
138
  functions,
139
+ extraImports: symbolsInfo ? (0, utils_1.resolveExtraImports)(importedPaths, functions, symbolsInfo) : [],
133
140
  });
134
141
  }
135
142
  for (const [name, content] of Object.entries(files)) {
@@ -18,6 +18,9 @@ import {vm} from "@chimera/Hevm.sol";
18
18
 
19
19
  // Helpers
20
20
  import {Panic} from "@recon/Panic.sol";
21
+ {{#each extraImports}}
22
+ import { {{this.name}} } from "{{this.path}}";
23
+ {{/each}}
21
24
 
22
25
  abstract contract AdminTargets is
23
26
  BaseTargetFunctions,
@@ -20,6 +20,9 @@ import {vm} from "@chimera/Hevm.sol";
20
20
  import {Panic} from "@recon/Panic.sol";
21
21
 
22
22
  {{#if path}}import "{{path}}";{{/if}}
23
+ {{#each extraImports}}
24
+ import { {{this.name}} } from "{{this.path}}";
25
+ {{/each}}
23
26
 
24
27
  abstract contract {{pascal contractName}}Targets is
25
28
  BaseTargetFunctions,
package/dist/types.d.ts CHANGED
@@ -98,3 +98,11 @@ export type CallTreeData = {
98
98
  function: FunctionDefinition;
99
99
  callTree: CallTree;
100
100
  };
101
+ export interface FoundryConfig {
102
+ src: string;
103
+ test: string;
104
+ script: string;
105
+ out: string;
106
+ libs: string[];
107
+ remappings: string[];
108
+ }
package/dist/utils.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { ASTNode, ContractDefinition, Assignment, FunctionCall, FunctionDefinition, SourceUnit, VariableDeclaration, EventDefinition, ErrorDefinition } from 'solc-typed-ast';
2
- import { CallType } from './types';
2
+ import { CallType, FunctionDefinitionParams, FoundryConfig, ParamDefinition } from './types';
3
3
  export declare function fileExists(p: string): Promise<boolean>;
4
4
  export declare function getFoundryConfigPath(workspaceRoot: string, override?: string): string;
5
5
  export declare function findOutputDirectory(workspaceRoot: string, foundryConfigPath: string): Promise<string>;
6
6
  export declare function findSrcDirectory(workspaceRoot: string, foundryConfigPath: string): Promise<string>;
7
7
  export declare function getTestFolder(foundryRoot: string): Promise<string>;
8
+ export declare const FOUNDRY_CONFIG_DEFAULTS: FoundryConfig;
9
+ export declare function getFoundryConfig(foundryRoot: string): Promise<FoundryConfig>;
8
10
  export declare function stripAnsiCodes(text: string): string;
9
11
  export declare function getEnvPath(): string;
10
12
  export declare const getLines: (content: string, src: string) => {
@@ -72,3 +74,30 @@ export declare const loadBuildInfoFromFile: (buildInfoPath: string, skipPatterns
72
74
  * @returns Object containing sourceUnits and raw buildOutput
73
75
  */
74
76
  export declare const loadLatestBuildInfo: (foundryRoot: string, outputDir?: string, skipPatterns?: string[]) => Promise<BuildInfoResult>;
77
+ /**
78
+ * Recursively walks parameter internalType fields and extracts referenced type names.
79
+ * For dotted types like "struct ISpoke.DynamicReserveConfig" → extracts "ISpoke"
80
+ * For "contract ISpoke" (no dot) → extracts "ISpoke"
81
+ * Skips primitives.
82
+ */
83
+ export declare function collectReferencedTypeNames(params: ParamDefinition[]): Set<string>;
84
+ export interface SymbolsInfo {
85
+ byFile: Map<string, Set<string>>;
86
+ definedIn: Map<string, string>;
87
+ }
88
+ /**
89
+ * Builds maps of exported symbols per file and definition locations from the raw build output.
90
+ * - byFile: file path → set of exported symbol names
91
+ * - definedIn: symbol name → file path where it's actually defined (top-level definitions)
92
+ */
93
+ export declare function buildExportedSymbolsMap(buildOutput: any): SymbolsInfo;
94
+ export interface ExtraImport {
95
+ name: string;
96
+ path: string;
97
+ }
98
+ /**
99
+ * Given imported file paths, function definitions, and symbol maps, returns a list of
100
+ * named imports needed for types referenced in function parameters but not available
101
+ * through already-imported files.
102
+ */
103
+ export declare function resolveExtraImports(importedPaths: string[], functions: FunctionDefinitionParams[], symbolsInfo: SymbolsInfo): ExtraImport[];
package/dist/utils.js CHANGED
@@ -36,12 +36,13 @@ 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 = void 0;
39
+ exports.loadLatestBuildInfo = 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;
43
43
  exports.findSrcDirectory = findSrcDirectory;
44
44
  exports.getTestFolder = getTestFolder;
45
+ exports.getFoundryConfig = getFoundryConfig;
45
46
  exports.stripAnsiCodes = stripAnsiCodes;
46
47
  exports.getEnvPath = getEnvPath;
47
48
  exports.highLevelCallWithOptions = highLevelCallWithOptions;
@@ -64,10 +65,14 @@ exports.resolveOverride = resolveOverride;
64
65
  exports.idToContract = idToContract;
65
66
  exports.forwardLinearization = forwardLinearization;
66
67
  exports.resolveSuper = resolveSuper;
68
+ exports.collectReferencedTypeNames = collectReferencedTypeNames;
69
+ exports.buildExportedSymbolsMap = buildExportedSymbolsMap;
70
+ exports.resolveExtraImports = resolveExtraImports;
67
71
  const fs = __importStar(require("fs/promises"));
68
72
  const path = __importStar(require("path"));
69
73
  const solc_typed_ast_1 = require("solc-typed-ast");
70
74
  const src_location_1 = __importDefault(require("src-location"));
75
+ const child_process_1 = require("child_process");
71
76
  const types_1 = require("./types");
72
77
  async function fileExists(p) {
73
78
  try {
@@ -125,6 +130,41 @@ async function getTestFolder(foundryRoot) {
125
130
  }
126
131
  return 'test';
127
132
  }
133
+ exports.FOUNDRY_CONFIG_DEFAULTS = {
134
+ src: 'src',
135
+ test: 'test',
136
+ script: 'script',
137
+ out: 'out',
138
+ libs: ['lib'],
139
+ remappings: [],
140
+ };
141
+ function getFoundryConfig(foundryRoot) {
142
+ return new Promise((resolve) => {
143
+ (0, child_process_1.exec)('forge config --json', {
144
+ cwd: foundryRoot,
145
+ env: { ...process.env, PATH: getEnvPath() },
146
+ }, (err, stdout) => {
147
+ if (err) {
148
+ resolve({ ...exports.FOUNDRY_CONFIG_DEFAULTS });
149
+ return;
150
+ }
151
+ try {
152
+ const raw = JSON.parse(stdout);
153
+ resolve({
154
+ src: typeof raw.src === 'string' ? raw.src : exports.FOUNDRY_CONFIG_DEFAULTS.src,
155
+ test: typeof raw.test === 'string' ? raw.test : exports.FOUNDRY_CONFIG_DEFAULTS.test,
156
+ script: typeof raw.script === 'string' ? raw.script : exports.FOUNDRY_CONFIG_DEFAULTS.script,
157
+ out: typeof raw.out === 'string' ? raw.out : exports.FOUNDRY_CONFIG_DEFAULTS.out,
158
+ libs: Array.isArray(raw.libs) ? raw.libs.filter((l) => typeof l === 'string') : exports.FOUNDRY_CONFIG_DEFAULTS.libs,
159
+ remappings: Array.isArray(raw.remappings) ? raw.remappings.filter((r) => typeof r === 'string') : exports.FOUNDRY_CONFIG_DEFAULTS.remappings,
160
+ });
161
+ }
162
+ catch {
163
+ resolve({ ...exports.FOUNDRY_CONFIG_DEFAULTS });
164
+ }
165
+ });
166
+ });
167
+ }
128
168
  function stripAnsiCodes(text) {
129
169
  return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
130
170
  }
@@ -517,3 +557,132 @@ const loadLatestBuildInfo = async (foundryRoot, outputDir = 'out', skipPatterns
517
557
  return (0, exports.loadBuildInfoFromFile)(latestFile, skipPatterns);
518
558
  };
519
559
  exports.loadLatestBuildInfo = loadLatestBuildInfo;
560
+ // --- Extra import resolution utilities ---
561
+ const PRIMITIVE_TYPES = new Set([
562
+ 'address', 'bool', 'string', 'bytes',
563
+ 'uint8', 'uint16', 'uint32', 'uint64', 'uint128', 'uint256',
564
+ 'int8', 'int16', 'int32', 'int64', 'int128', 'int256',
565
+ 'bytes1', 'bytes2', 'bytes3', 'bytes4', 'bytes5', 'bytes6', 'bytes7', 'bytes8',
566
+ 'bytes9', 'bytes10', 'bytes11', 'bytes12', 'bytes13', 'bytes14', 'bytes15', 'bytes16',
567
+ 'bytes17', 'bytes18', 'bytes19', 'bytes20', 'bytes21', 'bytes22', 'bytes23', 'bytes24',
568
+ 'bytes25', 'bytes26', 'bytes27', 'bytes28', 'bytes29', 'bytes30', 'bytes31', 'bytes32',
569
+ ]);
570
+ /**
571
+ * Recursively walks parameter internalType fields and extracts referenced type names.
572
+ * For dotted types like "struct ISpoke.DynamicReserveConfig" → extracts "ISpoke"
573
+ * For "contract ISpoke" (no dot) → extracts "ISpoke"
574
+ * Skips primitives.
575
+ */
576
+ function collectReferencedTypeNames(params) {
577
+ const names = new Set();
578
+ const walk = (p) => {
579
+ if (p.components && p.components.length > 0) {
580
+ for (const c of p.components) {
581
+ walk(c);
582
+ }
583
+ }
584
+ const raw = p.internalType;
585
+ if (!raw)
586
+ return;
587
+ // Strip prefix keywords: "struct ", "enum ", "contract "
588
+ let cleaned = raw.replace(/^(struct|enum|contract)\s+/, '').trim();
589
+ // Strip array suffixes like "[]" or "[5]"
590
+ cleaned = cleaned.replace(/(\[.*\])+$/, '');
591
+ if (!cleaned || PRIMITIVE_TYPES.has(cleaned))
592
+ return;
593
+ // If dotted, extract prefix (e.g., "ISpoke.DynamicReserveConfig" → "ISpoke")
594
+ const dotIdx = cleaned.indexOf('.');
595
+ if (dotIdx > 0) {
596
+ names.add(cleaned.substring(0, dotIdx));
597
+ }
598
+ else {
599
+ // Standalone type name (e.g., "ISpoke" from "contract ISpoke")
600
+ names.add(cleaned);
601
+ }
602
+ };
603
+ for (const p of params) {
604
+ walk(p);
605
+ }
606
+ return names;
607
+ }
608
+ /**
609
+ * Builds maps of exported symbols per file and definition locations from the raw build output.
610
+ * - byFile: file path → set of exported symbol names
611
+ * - definedIn: symbol name → file path where it's actually defined (top-level definitions)
612
+ */
613
+ function buildExportedSymbolsMap(buildOutput) {
614
+ const byFile = new Map();
615
+ const definedIn = new Map();
616
+ if (!(buildOutput === null || buildOutput === void 0 ? void 0 : buildOutput.sources)) {
617
+ return { byFile, definedIn };
618
+ }
619
+ for (const [filePath, sourceData] of Object.entries(buildOutput.sources)) {
620
+ const ast = sourceData === null || sourceData === void 0 ? void 0 : sourceData.ast;
621
+ if (!ast)
622
+ continue;
623
+ // Populate byFile from exportedSymbols
624
+ const exported = ast.exportedSymbols;
625
+ if (exported && typeof exported === 'object') {
626
+ const symbolSet = new Set(Object.keys(exported));
627
+ byFile.set(filePath, symbolSet);
628
+ }
629
+ // Populate definedIn from top-level AST nodes
630
+ const nodes = ast.nodes;
631
+ if (Array.isArray(nodes)) {
632
+ for (const node of nodes) {
633
+ const nodeType = node === null || node === void 0 ? void 0 : node.nodeType;
634
+ if (nodeType === 'ContractDefinition' ||
635
+ nodeType === 'StructDefinition' ||
636
+ nodeType === 'EnumDefinition' ||
637
+ nodeType === 'UserDefinedValueTypeDefinition') {
638
+ const name = node.name;
639
+ if (name && typeof name === 'string') {
640
+ definedIn.set(name, filePath);
641
+ }
642
+ }
643
+ }
644
+ }
645
+ }
646
+ return { byFile, definedIn };
647
+ }
648
+ /**
649
+ * Given imported file paths, function definitions, and symbol maps, returns a list of
650
+ * named imports needed for types referenced in function parameters but not available
651
+ * through already-imported files.
652
+ */
653
+ function resolveExtraImports(importedPaths, functions, symbolsInfo) {
654
+ // Collect all referenced type names from all function inputs and outputs
655
+ const allParams = [];
656
+ for (const fn of functions) {
657
+ if (fn.abi.inputs)
658
+ allParams.push(...fn.abi.inputs);
659
+ if (fn.abi.outputs)
660
+ allParams.push(...fn.abi.outputs);
661
+ }
662
+ const referenced = collectReferencedTypeNames(allParams);
663
+ if (referenced.size === 0)
664
+ return [];
665
+ // Collect available symbols from already-imported files
666
+ const available = new Set();
667
+ for (const importPath of importedPaths) {
668
+ const symbols = symbolsInfo.byFile.get(importPath);
669
+ if (symbols) {
670
+ for (const s of symbols) {
671
+ available.add(s);
672
+ }
673
+ }
674
+ }
675
+ // For each referenced type not available, find its definition file
676
+ const results = [];
677
+ const seen = new Set();
678
+ for (const typeName of Array.from(referenced).sort()) {
679
+ if (available.has(typeName))
680
+ continue;
681
+ const defFile = symbolsInfo.definedIn.get(typeName);
682
+ if (defFile && !seen.has(typeName)) {
683
+ seen.add(typeName);
684
+ results.push({ name: typeName, path: defFile });
685
+ }
686
+ }
687
+ return results;
688
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recon-generate",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {