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.
- package/dist/generator.d.ts +4 -0
- package/dist/generator.js +43 -21
- package/dist/index.js +6 -4
- package/dist/templateManager.d.ts +2 -1
- package/dist/templateManager.js +10 -3
- package/dist/templates/targets/admin-targets.js +3 -0
- package/dist/templates/targets/targets.js +3 -0
- package/dist/types.d.ts +8 -0
- package/dist/utils.d.ts +30 -1
- package/dist/utils.js +170 -1
- package/package.json +1 -1
package/dist/generator.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
'
|
|
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
|
|
340
|
-
const foundryRoot = path.dirname(
|
|
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) :
|
|
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:
|
|
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
|
}
|
package/dist/templateManager.js
CHANGED
|
@@ -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({
|
|
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:
|
|
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)) {
|
|
@@ -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
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
|
+
}
|