recon-generate 0.0.4 → 0.0.6

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/README.md CHANGED
@@ -32,6 +32,7 @@ Key options:
32
32
  - `--admin "A:{fnSig,fnSig}"` — mark listed functions as admin (`asAdmin`).
33
33
  - `--mock "A,B"` — generate mocks for the listed contracts (compiled ABIs required); mocks go under `recon[-name]/mocks` and are added to targets.
34
34
  - `--dynamic-deploy "Foo,Bar"` — keep dynamic lists for the listed contracts: deploys one instance, tracks addresses in an array, exposes `_getRandom<Contract>` and `switch<Contract>(entropy)` helpers to rotate among deployed instances.
35
+ - `--coverage` — emit `recon[-name]-coverage.json` with source line ranges for included contract functions.
35
36
  - `--name <suite>` — name the suite; affects folder (`recon-<name>`), config filenames (`echidna-<name>.yaml`, `medusa-<name>.json`, `halmos-<name>.toml`), and Crytic tester/runner names.
36
37
  - `--force` — replace existing generated suite output under `--output` (does **not** rebuild `.recon/out`).
37
38
  - `--force-build` — delete `.recon/out` to force a fresh compile before generation.
@@ -67,6 +68,19 @@ recon-generate --mock "Morpho,Other" --force
67
68
 
68
69
  # Enable dynamic deploy helpers for Foo and Bar
69
70
  recon-generate --dynamic-deploy "Foo,Bar" --force
71
+
72
+ # Link subcommand
73
+
74
+ After generating a suite you can update Echidna and Medusa configs with detected Foundry libraries:
75
+
76
+ ```bash
77
+ recon-generate link
78
+ ```
79
+
80
+ This runs `crytic-compile` with `--foundry-compile-all` to find linked libraries and rewrites:
81
+
82
+ - Echidna YAML `cryticArgs` and `deployContracts` with deterministic placeholder addresses
83
+ - Medusa `compilation.platformConfig.args` to include `--compile-libraries`
70
84
  ```
71
85
 
72
86
  ### Behavior
@@ -16,6 +16,7 @@ export interface GeneratorOptions {
16
16
  dynamicDeploy?: Set<string>;
17
17
  suiteNameSnake: string;
18
18
  suiteNamePascal: string;
19
+ coverage?: boolean;
19
20
  }
20
21
  export declare class ReconGenerator {
21
22
  private foundryRoot;
package/dist/generator.js CHANGED
@@ -43,8 +43,10 @@ const path = __importStar(require("path"));
43
43
  const abi_to_mock_1 = __importDefault(require("abi-to-mock"));
44
44
  const parser_1 = __importDefault(require("@solidity-parser/parser"));
45
45
  const templateManager_1 = require("./templateManager");
46
+ const processor_1 = require("./processor");
46
47
  const types_1 = require("./types");
47
48
  const utils_1 = require("./utils");
49
+ const solc_typed_ast_1 = require("solc-typed-ast");
48
50
  class ReconGenerator {
49
51
  constructor(foundryRoot, options) {
50
52
  this.foundryRoot = foundryRoot;
@@ -195,7 +197,7 @@ class ReconGenerator {
195
197
  return kind;
196
198
  }
197
199
  async findSourceContracts() {
198
- var _a;
200
+ var _a, _b;
199
201
  const contracts = [];
200
202
  const outDir = this.outDir();
201
203
  try {
@@ -224,7 +226,12 @@ class ReconGenerator {
224
226
  const lowerName = String(contractName).toLowerCase();
225
227
  const isGeneratedMock = this.generatedMocks.has(String(contractName));
226
228
  const contractKind = await this.getContractKind(sourcePath, String(contractName));
227
- if (contractKind === 'interface' || contractKind === 'library') {
229
+ // Check if contract is explicitly included or mocked - if so, don't filter by kind
230
+ const isExplicitlyIncluded = this.options.include &&
231
+ (this.options.include.contractOnly.has(String(contractName)) ||
232
+ this.options.include.functions.has(String(contractName)));
233
+ const isExplicitlyMocked = (_b = this.options.mocks) === null || _b === void 0 ? void 0 : _b.has(String(contractName));
234
+ if ((contractKind === 'interface' || contractKind === 'library') && !isExplicitlyIncluded && !isExplicitlyMocked) {
228
235
  this.logDebug('Skipping non-contract artifact', { contractName, sourcePath, contractKind });
229
236
  continue;
230
237
  }
@@ -388,7 +395,10 @@ class ReconGenerator {
388
395
  if (!tempSrc) {
389
396
  return;
390
397
  }
391
- const mocksDir = path.join(this.options.suiteDir, 'mocks');
398
+ const suiteDirAbs = path.isAbsolute(this.options.suiteDir)
399
+ ? this.options.suiteDir
400
+ : path.join(this.foundryRoot, this.options.suiteDir);
401
+ const mocksDir = path.join(suiteDirAbs, 'mocks');
392
402
  await fs.mkdir(mocksDir, { recursive: true });
393
403
  const tempFiles = await fs.readdir(tempSrc);
394
404
  for (const f of tempFiles) {
@@ -464,7 +474,7 @@ class ReconGenerator {
464
474
  return config;
465
475
  }
466
476
  async run() {
467
- var _a, _b;
477
+ var _a, _b, _c, _d, _e;
468
478
  await this.ensureFoundryConfigExists();
469
479
  const outPath = this.outDir();
470
480
  const mockTargets = (_a = this.options.mocks) !== null && _a !== void 0 ? _a : new Set();
@@ -485,6 +495,73 @@ class ReconGenerator {
485
495
  }
486
496
  await this.ensureBuild(); // first build to get ABIs
487
497
  }
498
+ let sourceUnits = [];
499
+ const reader = new solc_typed_ast_1.ASTReader();
500
+ try {
501
+ const buildInfoDir = path.join(this.outDir(), 'build-info');
502
+ this.logDebug('Scanning build-info directory', { buildInfoDir });
503
+ const allFiles = await fs.readdir(buildInfoDir);
504
+ const jsonFiles = allFiles.filter(f => f.endsWith('.json'));
505
+ const filesWithStats = await Promise.all(jsonFiles.map(async (f) => ({
506
+ name: f,
507
+ path: path.join(buildInfoDir, f),
508
+ mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime
509
+ })));
510
+ const files = filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
511
+ if (files.length === 0) {
512
+ console.warn('❌ No build-info JSON files found');
513
+ }
514
+ const latestFile = (_b = files[0]) === null || _b === void 0 ? void 0 : _b.path;
515
+ if (latestFile) {
516
+ const fileContent = await fs.readFile(latestFile, 'utf-8');
517
+ const buildInfo = JSON.parse(fileContent);
518
+ const buildOutput = (_c = buildInfo.output) !== null && _c !== void 0 ? _c : buildInfo;
519
+ if (buildOutput) {
520
+ const filteredAstData = { ...buildOutput };
521
+ if (filteredAstData.sources) {
522
+ const validSources = {};
523
+ // Paths to skip - test and script directories
524
+ const skipPatterns = [
525
+ 'test/',
526
+ 'tests/',
527
+ 'script/',
528
+ 'scripts/',
529
+ 'contracts/test/',
530
+ 'contracts/script/',
531
+ 'contracts/scripts/',
532
+ 'forge-std/'
533
+ ];
534
+ for (const [key, content] of Object.entries(filteredAstData.sources)) {
535
+ // Skip test and script files
536
+ const shouldSkip = skipPatterns.some(pattern => key.includes(pattern));
537
+ if (shouldSkip) {
538
+ this.logDebug('Skipping source (pattern match)', { key });
539
+ continue;
540
+ }
541
+ const ast = content.ast || content.legacyAST || content.AST;
542
+ if (ast && (ast.nodeType === "SourceUnit" || ast.name === "SourceUnit")) {
543
+ validSources[key] = content;
544
+ }
545
+ else {
546
+ this.logDebug('Skipping source (no source unit)', { key });
547
+ }
548
+ }
549
+ filteredAstData.sources = validSources;
550
+ this.logDebug('Filtered sources for AST read', { kept: Object.keys(validSources).length, keys: Object.keys(validSources) });
551
+ }
552
+ else {
553
+ this.logDebug('No sources present in build output');
554
+ }
555
+ try {
556
+ sourceUnits = reader.read(filteredAstData);
557
+ }
558
+ catch (e) {
559
+ }
560
+ }
561
+ }
562
+ }
563
+ catch (e) {
564
+ }
488
565
  // Generate mocks if requested, then rebuild to pick them up
489
566
  let tempMockSrc = null;
490
567
  if (mockTargets.size > 0) {
@@ -503,12 +580,63 @@ class ReconGenerator {
503
580
  await this.copyAndCleanupMocks(tempMockSrc);
504
581
  }
505
582
  const contracts = await this.findSourceContracts();
506
- const filteredContracts = contracts.filter(c => this.isContractAllowed(c.name));
507
- if (filteredContracts.length === 0) {
508
- this.logDebug('No contracts left after include/exclude filters');
583
+ if (this.options.include) {
584
+ const requested = new Set([
585
+ ...this.options.include.contractOnly,
586
+ ...Array.from(this.options.include.functions.keys()),
587
+ ]);
588
+ const discovered = new Set(contracts.map(c => c.name));
589
+ const missing = Array.from(requested).filter((n) => !discovered.has(n));
590
+ if (missing.length > 0) {
591
+ console.warn('[recon-generate] Include requested contracts not found in build:', missing);
592
+ }
509
593
  }
594
+ const filteredContracts = [];
595
+ const skippedContracts = [];
596
+ for (const c of contracts) {
597
+ if (this.isContractAllowed(c.name)) {
598
+ filteredContracts.push(c);
599
+ }
600
+ else {
601
+ skippedContracts.push({ name: c.name, reason: 'filtered by include/exclude/mocks' });
602
+ }
603
+ }
604
+ this.logDebug('Contracts after filters', { count: filteredContracts.length });
510
605
  const reconConfig = await this.loadReconConfig(filteredContracts);
511
- const tm = new templateManager_1.TemplateManager(this.foundryRoot, this.options.suiteDir, this.options.suiteNameSnake, this.options.suiteNamePascal, (_b = this.options.dynamicDeploy) !== null && _b !== void 0 ? _b : new Set());
606
+ if (this.options.coverage) {
607
+ if (!sourceUnits || sourceUnits.length === 0) {
608
+ console.warn('Coverage requested but no source units were available; skipping coverage file.');
609
+ }
610
+ else {
611
+ const contractFunctions = new Map();
612
+ for (const contract of filteredContracts) {
613
+ const cfg = reconConfig[contract.jsonPath];
614
+ if (!cfg || cfg.enabled === false) {
615
+ continue;
616
+ }
617
+ const fnSet = new Set();
618
+ for (const fn of (_d = cfg.functions) !== null && _d !== void 0 ? _d : []) {
619
+ if (fn === null || fn === void 0 ? void 0 : fn.signature) {
620
+ fnSet.add(String(fn.signature));
621
+ }
622
+ }
623
+ if (fnSet.size > 0) {
624
+ this.logDebug('Coverage: adding contract functions', { contract: contract.name, count: fnSet.size });
625
+ contractFunctions.set(contract.name, fnSet);
626
+ }
627
+ }
628
+ if (contractFunctions.size > 0) {
629
+ const coverage = await (0, processor_1.buildCoverageMap)(sourceUnits, this.foundryRoot, contractFunctions);
630
+ const coverageName = this.options.suiteNameSnake
631
+ ? `recon-${this.options.suiteNameSnake}-coverage.json`
632
+ : 'recon-coverage.json';
633
+ const coveragePath = path.join(this.foundryRoot, coverageName);
634
+ await fs.writeFile(coveragePath, JSON.stringify(coverage, null, 2));
635
+ this.logDebug('Wrote coverage file', { coveragePath, files: Object.keys(coverage).length });
636
+ }
637
+ }
638
+ }
639
+ 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());
512
640
  await tm.generateTemplates(filteredContracts, reconConfig);
513
641
  }
514
642
  async listAvailable() {
@@ -533,16 +661,16 @@ class ReconGenerator {
533
661
  if (fns.length > 0) {
534
662
  filtered.push({ name: contract.name, path: contract.path, functions: fns });
535
663
  }
536
- }
537
- console.log('Available contracts and functions (after filters):');
538
- for (const c of filtered) {
539
- console.log(`- ${c.name} :: ${c.path}`);
540
- for (const fn of c.functions) {
541
- console.log(` • ${fn}`);
664
+ console.log('Available contracts and functions (after filters):');
665
+ for (const c of filtered) {
666
+ console.log(`- ${c.name} :: ${c.path}`);
667
+ for (const fn of c.functions) {
668
+ console.log(` • ${fn}`);
669
+ }
670
+ }
671
+ if (filtered.length === 0) {
672
+ console.log('No contracts/functions match the current include/exclude filters.');
542
673
  }
543
- }
544
- if (filtered.length === 0) {
545
- console.log('No contracts/functions match the current include/exclude filters.');
546
674
  }
547
675
  }
548
676
  }
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ const path = __importStar(require("path"));
39
39
  const case_1 = require("case");
40
40
  const generator_1 = require("./generator");
41
41
  const utils_1 = require("./utils");
42
+ const link_1 = require("./link");
42
43
  function parseFilter(input) {
43
44
  if (!input)
44
45
  return undefined;
@@ -116,57 +117,92 @@ async function main() {
116
117
  .option('--mock <names>', 'Comma-separated contract names to generate mocks for')
117
118
  .option('--dynamic-deploy <names>', 'Comma-separated contract names to enable dynamic deploy lists')
118
119
  .option('--debug', 'Print debug info about filters and selection')
120
+ .option('--coverage', 'Write coverage information to recon-coverage.json (or recon-<name>-coverage.json)')
119
121
  .option('--list', 'List available contracts/functions (after filters) and exit')
120
122
  .option('--force', 'Replace existing generated suite output (under --output). Does not rebuild .recon/out.')
121
123
  .option('--force-build', 'Delete .recon/out to force a fresh forge build before generating')
124
+ .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)');
125
+ program
126
+ .command('link')
127
+ .description('Link library addresses into echidna/medusa configs via crytic-compile')
128
+ .option('--echidna-config <path>', 'Path to echidna yaml (defaults based on --name)')
129
+ .option('--medusa-config <path>', 'Path to medusa json (defaults based on --name)')
130
+ .option('--name <suite>', 'Suite name to pick config defaults (echidna-<name>.yaml / medusa-<name>.json)')
122
131
  .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
123
- .parse(process.argv);
124
- const opts = program.opts();
125
- const workspaceRoot = process.cwd();
126
- const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
127
- const foundryRoot = path.dirname(foundryConfig);
128
- const includeFilter = parseFilter(opts.include);
129
- const excludeFilter = parseFilter(opts.exclude);
130
- const adminFilter = parseFilter(opts.admin);
131
- const mockSet = opts.mock
132
- ? new Set(String(opts.mock).split(',').map((s) => s.trim()).filter(Boolean))
133
- : undefined;
134
- const dynamicDeploySet = opts.dynamicDeploy
135
- ? new Set(String(opts.dynamicDeploy).split(',').map((s) => s.trim()).filter(Boolean))
136
- : undefined;
137
- const suiteRaw = opts.name ? String(opts.name).trim() : '';
138
- const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
139
- const suitePascal = suiteRaw ? (0, case_1.pascal)(suiteRaw) : '';
140
- const suiteFolderName = suiteSnake ? `recon-${suiteSnake}` : 'recon';
141
- const baseOut = opts.output ? String(opts.output) : await (0, utils_1.getTestFolder)(foundryRoot);
142
- const baseOutPath = path.isAbsolute(baseOut) ? baseOut : path.join(foundryRoot, baseOut);
143
- const suiteDir = path.join(baseOutPath, suiteFolderName);
144
- const reconDefaultName = suiteSnake ? `recon-${suiteSnake}.json` : 'recon.json';
145
- const reconPath = opts.recon
146
- ? (path.isAbsolute(opts.recon) ? opts.recon : path.join(workspaceRoot, opts.recon))
147
- : (await (0, utils_1.fileExists)(path.join(foundryRoot, reconDefaultName))
148
- ? path.join(foundryRoot, reconDefaultName)
149
- : path.join(foundryRoot, '.recon', reconDefaultName));
150
- const generator = new generator_1.ReconGenerator(foundryRoot, {
151
- suiteDir,
152
- reconConfigPath: reconPath,
153
- foundryConfigPath: foundryConfig,
154
- include: includeFilter,
155
- exclude: excludeFilter,
156
- admin: adminFilter ? adminFilter.functions : undefined,
157
- debug: !!opts.debug,
158
- force: !!opts.force,
159
- forceBuild: !!opts.forceBuild,
160
- mocks: mockSet,
161
- dynamicDeploy: dynamicDeploySet,
162
- suiteNameSnake: suiteSnake,
163
- suiteNamePascal: suitePascal,
132
+ .action(async (opts) => {
133
+ const workspaceRoot = process.cwd();
134
+ const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
135
+ const foundryRoot = path.dirname(foundryConfig);
136
+ const suiteRaw = opts.name ? String(opts.name).trim() : '';
137
+ const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
138
+ const suffix = suiteSnake ? `-${suiteSnake}` : '';
139
+ const echidnaDefault = `echidna${suffix}.yaml`;
140
+ const medusaDefault = `medusa${suffix}.json`;
141
+ const echidnaConfigOpt = opts.echidnaConfig || echidnaDefault;
142
+ const medusaConfigOpt = opts.medusaConfig || medusaDefault;
143
+ const echidnaConfigPath = path.isAbsolute(echidnaConfigOpt)
144
+ ? echidnaConfigOpt
145
+ : path.join(foundryRoot, echidnaConfigOpt);
146
+ const medusaConfigPath = path.isAbsolute(medusaConfigOpt)
147
+ ? medusaConfigOpt
148
+ : path.join(foundryRoot, medusaConfigOpt);
149
+ await (0, link_1.runLink)(foundryRoot, echidnaConfigPath, medusaConfigPath);
164
150
  });
165
- if (opts.list) {
166
- await generator.listAvailable();
167
- return;
168
- }
169
- await generator.run();
151
+ program
152
+ .action(async (opts) => {
153
+ const workspaceRoot = process.cwd();
154
+ const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
155
+ const foundryRoot = path.dirname(foundryConfig);
156
+ const includeFilter = parseFilter(opts.include);
157
+ const excludeFilter = parseFilter(opts.exclude);
158
+ const adminFilter = parseFilter(opts.admin);
159
+ const mockSet = opts.mock
160
+ ? new Set(String(opts.mock).split(',').map((s) => s.trim()).filter(Boolean))
161
+ : undefined;
162
+ if (includeFilter && mockSet && mockSet.size > 0) {
163
+ for (const m of mockSet) {
164
+ includeFilter.contractOnly.add(`${m}Mock`);
165
+ }
166
+ }
167
+ const dynamicDeploySet = opts.dynamicDeploy
168
+ ? new Set(String(opts.dynamicDeploy).split(',').map((s) => s.trim()).filter(Boolean))
169
+ : undefined;
170
+ const suiteRaw = opts.name ? String(opts.name).trim() : '';
171
+ const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
172
+ const suitePascal = suiteRaw ? (0, case_1.pascal)(suiteRaw) : '';
173
+ const suiteFolderName = suiteSnake ? `recon-${suiteSnake}` : 'recon';
174
+ const baseOut = opts.output ? String(opts.output) : await (0, utils_1.getTestFolder)(foundryRoot);
175
+ const baseOutPath = path.isAbsolute(baseOut) ? baseOut : path.join(foundryRoot, baseOut);
176
+ const suiteDir = path.join(baseOutPath, suiteFolderName);
177
+ const reconDefaultName = suiteSnake ? `recon-${suiteSnake}.json` : 'recon.json';
178
+ const reconPath = opts.recon
179
+ ? (path.isAbsolute(opts.recon) ? opts.recon : path.join(workspaceRoot, opts.recon))
180
+ : (await (0, utils_1.fileExists)(path.join(foundryRoot, reconDefaultName))
181
+ ? path.join(foundryRoot, reconDefaultName)
182
+ : path.join(foundryRoot, '.recon', reconDefaultName));
183
+ const generator = new generator_1.ReconGenerator(foundryRoot, {
184
+ suiteDir,
185
+ reconConfigPath: reconPath,
186
+ foundryConfigPath: foundryConfig,
187
+ include: includeFilter,
188
+ exclude: excludeFilter,
189
+ admin: adminFilter ? adminFilter.functions : undefined,
190
+ debug: !!opts.debug,
191
+ force: !!opts.force,
192
+ forceBuild: !!opts.forceBuild,
193
+ mocks: mockSet,
194
+ dynamicDeploy: dynamicDeploySet,
195
+ suiteNameSnake: suiteSnake,
196
+ suiteNamePascal: suitePascal,
197
+ coverage: !!opts.coverage,
198
+ });
199
+ if (opts.list) {
200
+ await generator.listAvailable();
201
+ return;
202
+ }
203
+ await generator.run();
204
+ });
205
+ await program.parseAsync(process.argv);
170
206
  }
171
207
  main().catch(err => {
172
208
  console.error('recon-generate failed:', err.message || err);
package/dist/link.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function runLink(foundryRoot: string, echidnaConfigPath: string, medusaConfigPath: string): Promise<void>;
package/dist/link.js ADDED
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.runLink = runLink;
40
+ const child_process_1 = require("child_process");
41
+ const fs = __importStar(require("fs/promises"));
42
+ const yaml_1 = __importDefault(require("yaml"));
43
+ const utils_1 = require("./utils");
44
+ const generateHexAddress = (index) => {
45
+ return `0xf${(index + 1).toString().padStart(2, '0')}`;
46
+ };
47
+ const parseLibrariesFromOutput = (output) => {
48
+ const usesPattern = /^\s+uses: \[(.*?)\]/gm;
49
+ const matches = [...(0, utils_1.stripAnsiCodes)(output).matchAll(usesPattern)];
50
+ const allLibraries = [];
51
+ for (const match of matches) {
52
+ if (match[1]) {
53
+ const libs = match[1]
54
+ .split(',')
55
+ .map(lib => lib.trim().replace(/["'\s]/g, ''))
56
+ .filter(lib => lib.length > 0);
57
+ for (const lib of libs) {
58
+ if (!allLibraries.includes(lib)) {
59
+ allLibraries.push(lib);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ return allLibraries;
65
+ };
66
+ const runCryticCompile = (cwd) => {
67
+ return new Promise((resolve, reject) => {
68
+ (0, child_process_1.exec)('crytic-compile . --foundry-compile-all --print-libraries', { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (error, stdout, stderr) => {
69
+ if (error) {
70
+ reject(new Error(stderr || error.message));
71
+ return;
72
+ }
73
+ resolve(stdout || '');
74
+ });
75
+ });
76
+ };
77
+ const formatEchidnaYaml = (config, libraries) => {
78
+ const cfg = { ...config };
79
+ delete cfg.cryticArgs;
80
+ delete cfg.deployContracts;
81
+ let out = yaml_1.default.stringify(cfg, { indent: 2 });
82
+ if (libraries.length === 0) {
83
+ out += 'cryticArgs: []\n';
84
+ out += 'deployContracts: []\n';
85
+ return out;
86
+ }
87
+ const libraryArgs = libraries.map((lib, index) => `(${lib},${generateHexAddress(index)})`).join(',');
88
+ out += `cryticArgs: ["--compile-libraries=${libraryArgs}","--foundry-compile-all"]\n`;
89
+ out += 'deployContracts:\n';
90
+ libraries.forEach((lib, index) => {
91
+ out += ` - ["${generateHexAddress(index)}", "${lib}"]\n`;
92
+ });
93
+ return out;
94
+ };
95
+ const updateEchidnaConfig = async (configPath, libraries) => {
96
+ const content = await fs.readFile(configPath, 'utf8');
97
+ const parsed = yaml_1.default.parse(content) || {};
98
+ const updated = formatEchidnaYaml(parsed, libraries);
99
+ await fs.writeFile(configPath, updated, 'utf8');
100
+ };
101
+ const updateMedusaConfig = async (configPath, libraries) => {
102
+ const content = await fs.readFile(configPath, 'utf8');
103
+ const parsed = JSON.parse(content);
104
+ if (!parsed.compilation)
105
+ parsed.compilation = {};
106
+ if (!parsed.compilation.platformConfig)
107
+ parsed.compilation.platformConfig = {};
108
+ if (libraries.length > 0) {
109
+ const libraryArgs = libraries.map((lib, index) => `(${lib},${generateHexAddress(index)})`).join(',');
110
+ parsed.compilation.platformConfig.args = ['--compile-libraries', libraryArgs, '--foundry-compile-all'];
111
+ }
112
+ else {
113
+ delete parsed.compilation.platformConfig.args;
114
+ }
115
+ await fs.writeFile(configPath, JSON.stringify(parsed, null, 2), 'utf8');
116
+ };
117
+ async function runLink(foundryRoot, echidnaConfigPath, medusaConfigPath) {
118
+ const output = await runCryticCompile(foundryRoot);
119
+ const libraries = parseLibrariesFromOutput(output);
120
+ console.log('Detected libraries:', libraries.length ? libraries.join(', ') : '(none)');
121
+ try {
122
+ await fs.access(echidnaConfigPath);
123
+ await updateEchidnaConfig(echidnaConfigPath, libraries);
124
+ console.log(`Updated echidna config at ${echidnaConfigPath}`);
125
+ }
126
+ catch (e) {
127
+ throw new Error(`Failed to update echidna config: ${e}`);
128
+ }
129
+ try {
130
+ await fs.access(medusaConfigPath);
131
+ await updateMedusaConfig(medusaConfigPath, libraries);
132
+ console.log(`Updated medusa config at ${medusaConfigPath}`);
133
+ }
134
+ catch (e) {
135
+ throw new Error(`Failed to update medusa config: ${e}`);
136
+ }
137
+ }
@@ -0,0 +1,3 @@
1
+ import * as $ from 'solc-typed-ast';
2
+ export declare const processContract: (contract: $.ContractDefinition) => Record<string, any>;
3
+ export declare function buildCoverageMap(asts: $.SourceUnit[], foundryRoot: string, contractFunctions: Map<string, Set<string>>): Promise<Record<string, string[]>>;
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.processContract = void 0;
37
+ exports.buildCoverageMap = buildCoverageMap;
38
+ const fs = __importStar(require("fs/promises"));
39
+ const path = __importStar(require("path"));
40
+ const $ = __importStar(require("solc-typed-ast"));
41
+ const types_1 = require("./types");
42
+ const utils_1 = require("./utils");
43
+ const stripDataLocation = (raw) => {
44
+ if (!raw) {
45
+ return '';
46
+ }
47
+ const noLocation = raw
48
+ .replace(/\s+(storage|memory|calldata)(\s+pointer)?/g, '')
49
+ .replace(/\s+pointer/g, '');
50
+ const noPrefix = noLocation
51
+ .replace(/^struct\s+/, '')
52
+ .replace(/^enum\s+/, '')
53
+ .replace(/^contract\s+/, '')
54
+ .trim();
55
+ return noPrefix;
56
+ };
57
+ const simplifyParamType = (t) => {
58
+ const arraySuffixMatch = t.match(/(\[.*\])$/);
59
+ const arraySuffix = arraySuffixMatch ? arraySuffixMatch[1] : '';
60
+ const base = arraySuffix ? t.slice(0, -arraySuffix.length) : t;
61
+ const segments = base.split('.');
62
+ const simple = segments[segments.length - 1];
63
+ return `${simple}${arraySuffix}`;
64
+ };
65
+ const simplifySignature = (sig) => {
66
+ const match = sig.match(/^(.*?)\((.*)\)$/);
67
+ if (!match)
68
+ return sig;
69
+ const name = match[1];
70
+ const params = match[2];
71
+ if (!params)
72
+ return `${name}()`;
73
+ const simplifiedParams = params
74
+ .split(',')
75
+ .map((p) => simplifyParamType(p.trim()))
76
+ .join(',');
77
+ return `${name}(${simplifiedParams})`;
78
+ };
79
+ const signatureFromFnDef = (fnDef) => {
80
+ const params = fnDef.vParameters.vParameters
81
+ .map((p) => { var _a; return stripDataLocation((_a = p.typeString) !== null && _a !== void 0 ? _a : ''); })
82
+ .join(',');
83
+ const name = fnDef.name
84
+ || (fnDef.kind === $.FunctionKind.Constructor
85
+ ? 'constructor'
86
+ : fnDef.kind === $.FunctionKind.Fallback
87
+ ? 'fallback'
88
+ : fnDef.kind === $.FunctionKind.Receive
89
+ ? 'receive'
90
+ : '');
91
+ return `${name}(${params})`;
92
+ };
93
+ const matchesSignature = (sig, allowed) => {
94
+ if (allowed.has(sig))
95
+ return true;
96
+ const nameOnly = sig.split('(')[0];
97
+ if (allowed.has(nameOnly))
98
+ return true;
99
+ const simplifiedSig = simplifySignature(sig);
100
+ const simplifiedName = simplifiedSig.split('(')[0];
101
+ if (allowed.has(simplifiedSig) || allowed.has(simplifiedName))
102
+ return true;
103
+ const simplifiedAllowed = new Set(Array.from(allowed).map((s) => simplifySignature(s)));
104
+ if (simplifiedAllowed.has(sig) || simplifiedAllowed.has(nameOnly))
105
+ return true;
106
+ if (simplifiedAllowed.has(simplifiedSig) || simplifiedAllowed.has(simplifiedName))
107
+ return true;
108
+ return false;
109
+ };
110
+ const processFunction = (fnDef, includeDeps = false) => {
111
+ const result = [];
112
+ fnDef.walk((n) => {
113
+ if ('vReferencedDeclaration' in n &&
114
+ n.vReferencedDeclaration &&
115
+ n.vReferencedDeclaration !== fnDef) {
116
+ if (n.vReferencedDeclaration instanceof $.FunctionDefinition ||
117
+ n.vReferencedDeclaration instanceof $.ModifierDefinition) {
118
+ const refSourceUnit = n.vReferencedDeclaration.getClosestParentByType($.SourceUnit);
119
+ if (result.some((x) => x.ast === n.vReferencedDeclaration)) {
120
+ return;
121
+ }
122
+ result.push({
123
+ ast: n.vReferencedDeclaration,
124
+ children: processFunction(n.vReferencedDeclaration, includeDeps),
125
+ callType: n instanceof $.FunctionCall ? (0, utils_1.getCallType)(n) : types_1.CallType.Internal,
126
+ });
127
+ }
128
+ }
129
+ });
130
+ return result;
131
+ };
132
+ const processContract = (contract) => {
133
+ const result = {};
134
+ result['vFunctions'] = [];
135
+ const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
136
+ for (const fnDef of allFunctions.filter((x) => x.implemented)) {
137
+ if (fnDef.visibility !== $.FunctionVisibility.External &&
138
+ fnDef.visibility !== $.FunctionVisibility.Public) {
139
+ continue;
140
+ }
141
+ if ((fnDef.stateMutability === $.FunctionStateMutability.Pure ||
142
+ fnDef.stateMutability === $.FunctionStateMutability.View)) {
143
+ continue;
144
+ }
145
+ if (!result['vFunctions'].some((x) => (0, utils_1.signatureEquals)(x, fnDef))) {
146
+ const rec = {
147
+ ast: fnDef,
148
+ children: processFunction(fnDef),
149
+ };
150
+ result['vFunctions'].push(rec);
151
+ }
152
+ }
153
+ return result;
154
+ };
155
+ exports.processContract = processContract;
156
+ async function buildCoverageMap(asts, foundryRoot, contractFunctions) {
157
+ const coverage = new Map();
158
+ const fileCache = new Map();
159
+ let nodesVisited = 0;
160
+ let nodesAdded = 0;
161
+ const addNodeRange = async (node) => {
162
+ var _a;
163
+ nodesVisited++;
164
+ const srcUnit = node.getClosestParentByType($.SourceUnit);
165
+ if (!srcUnit) {
166
+ return;
167
+ }
168
+ const parentContract = node.getClosestParentByType($.ContractDefinition);
169
+ if (parentContract && parentContract.kind === 'interface') {
170
+ return;
171
+ }
172
+ const absPath = path.isAbsolute(srcUnit.absolutePath)
173
+ ? srcUnit.absolutePath
174
+ : path.join(foundryRoot, srcUnit.absolutePath);
175
+ let content = fileCache.get(absPath);
176
+ if (!content) {
177
+ try {
178
+ content = await fs.readFile(absPath, 'utf8');
179
+ fileCache.set(absPath, content);
180
+ }
181
+ catch {
182
+ return;
183
+ }
184
+ }
185
+ const src = node.src;
186
+ if (!src) {
187
+ return;
188
+ }
189
+ const { start, end } = (0, utils_1.getLines)(content, src);
190
+ const key = path.relative(foundryRoot, absPath);
191
+ const entry = (_a = coverage.get(key)) !== null && _a !== void 0 ? _a : new Set();
192
+ entry.add(start === end ? `${start}` : `${start}-${end}`);
193
+ coverage.set(key, entry);
194
+ nodesAdded++;
195
+ };
196
+ const walkRecord = async (rec) => {
197
+ await addNodeRange(rec.ast);
198
+ for (const child of rec.children) {
199
+ await walkRecord(child);
200
+ }
201
+ };
202
+ for (const ast of asts) {
203
+ for (const contract of ast.getChildrenByType($.ContractDefinition)) {
204
+ if (contract.kind === 'interface') {
205
+ continue;
206
+ }
207
+ const allowedFns = contractFunctions.get(contract.name);
208
+ if (!allowedFns || allowedFns.size === 0) {
209
+ continue;
210
+ }
211
+ const processed = (0, exports.processContract)(contract);
212
+ const recs = (processed['vFunctions'] || []);
213
+ if (recs.length === 0) {
214
+ continue;
215
+ }
216
+ for (const rec of recs) {
217
+ const sig = signatureFromFnDef(rec.ast);
218
+ if (!matchesSignature(sig, allowedFns)) {
219
+ continue;
220
+ }
221
+ await walkRecord(rec);
222
+ }
223
+ }
224
+ }
225
+ const normalized = {};
226
+ for (const [relPath, ranges] of coverage.entries()) {
227
+ const sorted = Array.from(ranges).sort((a, b) => {
228
+ const start = (val) => parseInt(val.split('-')[0], 10);
229
+ return start(a) - start(b);
230
+ });
231
+ normalized[relPath] = sorted;
232
+ }
233
+ return normalized;
234
+ }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ASTNode } from "solc-typed-ast";
1
2
  export declare enum Actor {
2
3
  ACTOR = "actor",
3
4
  ADMIN = "admin"
@@ -43,3 +44,13 @@ export interface FunctionDefinitionParams {
43
44
  mode: Mode;
44
45
  separated?: boolean;
45
46
  }
47
+ export declare enum CallType {
48
+ Internal = "internal",
49
+ HighLevel = "high-level",
50
+ LowLevel = "low-level"
51
+ }
52
+ export type RecordItem = {
53
+ ast: ASTNode;
54
+ children: RecordItem[];
55
+ callType?: CallType;
56
+ };
package/dist/types.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Mode = exports.Actor = void 0;
3
+ exports.CallType = exports.Mode = exports.Actor = void 0;
4
4
  var Actor;
5
5
  (function (Actor) {
6
6
  Actor["ACTOR"] = "actor";
@@ -12,3 +12,9 @@ var Mode;
12
12
  Mode["FAIL"] = "fail";
13
13
  Mode["CATCH"] = "catch";
14
14
  })(Mode || (exports.Mode = Mode = {}));
15
+ var CallType;
16
+ (function (CallType) {
17
+ CallType["Internal"] = "internal";
18
+ CallType["HighLevel"] = "high-level";
19
+ CallType["LowLevel"] = "low-level";
20
+ })(CallType || (exports.CallType = CallType = {}));
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { ASTNode, ContractDefinition, Assignment, FunctionCall, FunctionDefinition, VariableDeclaration } from 'solc-typed-ast';
2
+ import { CallType } from './types';
1
3
  export declare function fileExists(p: string): Promise<boolean>;
2
4
  export declare function getFoundryConfigPath(workspaceRoot: string, override?: string): string;
3
5
  export declare function findOutputDirectory(workspaceRoot: string, foundryConfigPath: string): Promise<string>;
@@ -5,3 +7,25 @@ export declare function findSrcDirectory(workspaceRoot: string, foundryConfigPat
5
7
  export declare function getTestFolder(foundryRoot: string): Promise<string>;
6
8
  export declare function stripAnsiCodes(text: string): string;
7
9
  export declare function getEnvPath(): string;
10
+ export declare const getLines: (content: string, src: string) => {
11
+ start: any;
12
+ end: any;
13
+ };
14
+ export declare const getSource: (content: string, src: string) => string;
15
+ export declare const getIndex: (content: string, index: number) => number;
16
+ export declare function highLevelCallWithOptions(fnCall: FunctionCall, noStatic?: boolean): boolean;
17
+ export declare function highLevelCall(fnCall: FunctionCall, noStatic?: boolean): boolean;
18
+ export declare function lowLevelCallWithOptions(fnCall: FunctionCall): boolean;
19
+ export declare function lowLevelCall(fnCall: FunctionCall): boolean;
20
+ export declare function lowLevelStaticCall(fnCall: FunctionCall): boolean;
21
+ export declare function lowLevelDelegateCall(fnCall: FunctionCall): boolean;
22
+ export declare function lowLevelSend(fnCall: FunctionCall): boolean;
23
+ export declare function lowLevelTransfer(fnCall: FunctionCall): boolean;
24
+ export declare function isStateVarAssignment(node: Assignment): boolean;
25
+ export declare function getStateVarAssignment(node: Assignment): VariableDeclaration | null;
26
+ export declare function getDeepRef(node: ASTNode): ASTNode | undefined;
27
+ export declare function getDefinitions(contract: ContractDefinition, kind: string, inclusion?: boolean): ASTNode[];
28
+ export declare function toSource(node: ASTNode, version?: string): string;
29
+ export declare function getCallType(fnCall: FunctionCall): CallType;
30
+ export declare const getFunctionName: (fnDef: FunctionDefinition) => string;
31
+ export declare const signatureEquals: (a: FunctionDefinition, b: FunctionDefinition) => boolean;
package/dist/utils.js CHANGED
@@ -32,7 +32,11 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.signatureEquals = exports.getFunctionName = exports.getIndex = exports.getSource = exports.getLines = void 0;
36
40
  exports.fileExists = fileExists;
37
41
  exports.getFoundryConfigPath = getFoundryConfigPath;
38
42
  exports.findOutputDirectory = findOutputDirectory;
@@ -40,8 +44,25 @@ exports.findSrcDirectory = findSrcDirectory;
40
44
  exports.getTestFolder = getTestFolder;
41
45
  exports.stripAnsiCodes = stripAnsiCodes;
42
46
  exports.getEnvPath = getEnvPath;
47
+ exports.highLevelCallWithOptions = highLevelCallWithOptions;
48
+ exports.highLevelCall = highLevelCall;
49
+ exports.lowLevelCallWithOptions = lowLevelCallWithOptions;
50
+ exports.lowLevelCall = lowLevelCall;
51
+ exports.lowLevelStaticCall = lowLevelStaticCall;
52
+ exports.lowLevelDelegateCall = lowLevelDelegateCall;
53
+ exports.lowLevelSend = lowLevelSend;
54
+ exports.lowLevelTransfer = lowLevelTransfer;
55
+ exports.isStateVarAssignment = isStateVarAssignment;
56
+ exports.getStateVarAssignment = getStateVarAssignment;
57
+ exports.getDeepRef = getDeepRef;
58
+ exports.getDefinitions = getDefinitions;
59
+ exports.toSource = toSource;
60
+ exports.getCallType = getCallType;
43
61
  const fs = __importStar(require("fs/promises"));
44
62
  const path = __importStar(require("path"));
63
+ const solc_typed_ast_1 = require("solc-typed-ast");
64
+ const src_location_1 = __importDefault(require("src-location"));
65
+ const types_1 = require("./types");
45
66
  async function fileExists(p) {
46
67
  try {
47
68
  await fs.access(p);
@@ -104,3 +125,211 @@ function stripAnsiCodes(text) {
104
125
  function getEnvPath() {
105
126
  return process.env.PATH || '';
106
127
  }
128
+ const getLines = (content, src) => {
129
+ let en = (0, exports.getIndex)(content, parseInt(src.split(':')[0]) + parseInt(src.split(':')[1]));
130
+ let { line: start } = src_location_1.default.indexToLocation(content, (0, exports.getIndex)(content, parseInt(src.split(':')[0])), true);
131
+ let { line: end } = src_location_1.default.indexToLocation(content, en, true);
132
+ return { start, end };
133
+ };
134
+ exports.getLines = getLines;
135
+ const getSource = (content, src) => {
136
+ let en = (0, exports.getIndex)(content, parseInt(src.split(':')[0]) + parseInt(src.split(':')[1]));
137
+ return content.slice((0, exports.getIndex)(content, parseInt(src.split(':')[0])), en);
138
+ };
139
+ exports.getSource = getSource;
140
+ const getIndex = (content, index) => {
141
+ const sourceBytes = (new TextEncoder).encode(content);
142
+ return new TextDecoder().decode(sourceBytes.slice(0, index)).length;
143
+ };
144
+ exports.getIndex = getIndex;
145
+ function highLevelCallWithOptions(fnCall, noStatic = false) {
146
+ var _a, _b;
147
+ if (!(fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess) ||
148
+ !(fnCall.vExpression.vExpression instanceof solc_typed_ast_1.MemberAccess))
149
+ return false;
150
+ const ref = (_a = fnCall.vExpression) === null || _a === void 0 ? void 0 : _a.vExpression.vReferencedDeclaration;
151
+ if (!(ref instanceof solc_typed_ast_1.FunctionDefinition))
152
+ return false;
153
+ if (noStatic) {
154
+ if (ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.Pure ||
155
+ ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.View ||
156
+ ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.Constant) {
157
+ return false;
158
+ }
159
+ }
160
+ if (fnCall.vExpression.vExpression.vExpression.typeString.startsWith('type(library ')) {
161
+ if (!ref.vReturnParameters || ((_b = ref.vReturnParameters) === null || _b === void 0 ? void 0 : _b.vParameters.length) === 0)
162
+ return false;
163
+ for (const inFnCall of ref.getChildrenByType(solc_typed_ast_1.FunctionCall)) {
164
+ if (highLevelCall(inFnCall, noStatic))
165
+ return true;
166
+ if (highLevelCallWithOptions(inFnCall, noStatic))
167
+ return true;
168
+ }
169
+ }
170
+ return (fnCall.vExpression.vExpression.vExpression.typeString.startsWith('contract ') ||
171
+ fnCall.vExpression.vExpression.typeString === 'address');
172
+ }
173
+ function highLevelCall(fnCall, noStatic = false) {
174
+ var _a, _b, _c;
175
+ if (!(fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess))
176
+ return false;
177
+ const ref = fnCall.vExpression.vReferencedDeclaration;
178
+ if (!(ref instanceof solc_typed_ast_1.FunctionDefinition))
179
+ return false;
180
+ if (noStatic) {
181
+ if (ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.Pure ||
182
+ ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.View ||
183
+ ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.Constant) {
184
+ return false;
185
+ }
186
+ }
187
+ if (fnCall.vExpression.vExpression.typeString.startsWith('type(library ')) {
188
+ if (!ref.vReturnParameters || ((_a = ref.vReturnParameters) === null || _a === void 0 ? void 0 : _a.vParameters.length) === 0)
189
+ return false;
190
+ for (const inFnCall of ref.getChildrenByType(solc_typed_ast_1.FunctionCall)) {
191
+ if (highLevelCall(inFnCall, noStatic))
192
+ return true;
193
+ if (highLevelCallWithOptions(inFnCall, noStatic))
194
+ return true;
195
+ }
196
+ }
197
+ return (fnCall.vExpression.vExpression.typeString.startsWith('contract ') ||
198
+ ((_c = (_b = fnCall.vExpression) === null || _b === void 0 ? void 0 : _b.vExpression) === null || _c === void 0 ? void 0 : _c.typeString) === 'address');
199
+ }
200
+ function lowLevelCallWithOptions(fnCall) {
201
+ return (fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess &&
202
+ fnCall.vExpression.vExpression instanceof solc_typed_ast_1.MemberAccess &&
203
+ fnCall.vExpression.vExpression.memberName === 'call');
204
+ }
205
+ function lowLevelCall(fnCall) {
206
+ if (fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess && fnCall.vExpression.memberName === 'call') {
207
+ return true;
208
+ }
209
+ else if (lowLevelCallWithOptions(fnCall)) {
210
+ return true;
211
+ }
212
+ return false;
213
+ }
214
+ function lowLevelStaticCall(fnCall) {
215
+ if (fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess &&
216
+ fnCall.vExpression.memberName === 'staticcall') {
217
+ return true;
218
+ }
219
+ else if (fnCall.vExpression instanceof solc_typed_ast_1.FunctionCall &&
220
+ fnCall.vExpression.vExpression instanceof solc_typed_ast_1.MemberAccess &&
221
+ fnCall.vExpression.vExpression.memberName === 'staticcall') {
222
+ return true;
223
+ }
224
+ return false;
225
+ }
226
+ function lowLevelDelegateCall(fnCall) {
227
+ if (fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess &&
228
+ fnCall.vExpression.memberName === 'delegatecall') {
229
+ return true;
230
+ }
231
+ else if (fnCall.vExpression instanceof solc_typed_ast_1.FunctionCall &&
232
+ fnCall.vExpression.vExpression instanceof solc_typed_ast_1.MemberAccess &&
233
+ fnCall.vExpression.vExpression.memberName === 'delegatecall') {
234
+ return true;
235
+ }
236
+ return false;
237
+ }
238
+ function lowLevelSend(fnCall) {
239
+ if (fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess && fnCall.vExpression.memberName === 'send') {
240
+ return true;
241
+ }
242
+ else if (fnCall.vExpression instanceof solc_typed_ast_1.FunctionCall &&
243
+ fnCall.vExpression.vExpression instanceof solc_typed_ast_1.MemberAccess &&
244
+ fnCall.vExpression.vExpression.memberName === 'send') {
245
+ return true;
246
+ }
247
+ return false;
248
+ }
249
+ function lowLevelTransfer(fnCall) {
250
+ if (fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess && fnCall.vExpression.memberName === 'transfer') {
251
+ return true;
252
+ }
253
+ else if (fnCall.vExpression instanceof solc_typed_ast_1.FunctionCall &&
254
+ fnCall.vExpression.vExpression instanceof solc_typed_ast_1.MemberAccess &&
255
+ fnCall.vExpression.vExpression.memberName === 'transfer') {
256
+ return true;
257
+ }
258
+ return false;
259
+ }
260
+ function isStateVarAssignment(node) {
261
+ const decl = getStateVarAssignment(node);
262
+ if (!decl)
263
+ return false;
264
+ return decl && (decl.stateVariable || decl.storageLocation === solc_typed_ast_1.DataLocation.Storage);
265
+ }
266
+ function getStateVarAssignment(node) {
267
+ const decl = getDeepRef(node.vLeftHandSide);
268
+ if (!(decl instanceof solc_typed_ast_1.VariableDeclaration))
269
+ return null;
270
+ return decl;
271
+ }
272
+ function getDeepRef(node) {
273
+ if (node instanceof solc_typed_ast_1.Identifier) {
274
+ return node.vReferencedDeclaration;
275
+ }
276
+ else if (node instanceof solc_typed_ast_1.IndexAccess) {
277
+ return getDeepRef(node.vBaseExpression);
278
+ }
279
+ else if (node instanceof solc_typed_ast_1.MemberAccess) {
280
+ return getDeepRef(node.vExpression);
281
+ }
282
+ else {
283
+ return undefined;
284
+ }
285
+ }
286
+ function getDefinitions(contract, kind, inclusion = true) {
287
+ let defs = inclusion ? contract[kind] : [];
288
+ for (const child of contract.vLinearizedBaseContracts.filter((x) => x !== contract)) {
289
+ defs = getDefinitions(child, kind).concat(defs.filter((x) => !getDefinitions(child, kind).includes(x)));
290
+ }
291
+ return defs;
292
+ }
293
+ function toSource(node, version) {
294
+ const formatter = new solc_typed_ast_1.PrettyFormatter(4, 0);
295
+ const writer = new solc_typed_ast_1.ASTWriter(solc_typed_ast_1.DefaultASTWriterMapping, formatter, version ? version : solc_typed_ast_1.LatestCompilerVersion);
296
+ return writer.write(node);
297
+ }
298
+ function getCallType(fnCall) {
299
+ if (highLevelCall(fnCall)) {
300
+ return types_1.CallType.HighLevel;
301
+ }
302
+ else if (lowLevelCall(fnCall)) {
303
+ return types_1.CallType.LowLevel;
304
+ }
305
+ return types_1.CallType.Internal;
306
+ }
307
+ const getFunctionName = (fnDef) => {
308
+ if (fnDef.name) {
309
+ return fnDef.name;
310
+ }
311
+ if (fnDef.isConstructor || fnDef.kind === solc_typed_ast_1.FunctionKind.Constructor) {
312
+ return 'constructor';
313
+ }
314
+ if (fnDef.kind === solc_typed_ast_1.FunctionKind.Fallback) {
315
+ return 'fallback';
316
+ }
317
+ if (fnDef.kind === solc_typed_ast_1.FunctionKind.Receive) {
318
+ return 'receive';
319
+ }
320
+ return 'Unknown';
321
+ };
322
+ exports.getFunctionName = getFunctionName;
323
+ const signatureEquals = (a, b) => {
324
+ if (!a.name || !b.name)
325
+ return false;
326
+ if (a.name !== b.name)
327
+ return false;
328
+ if (a.vParameters.vParameters.map((x) => x.type).join(',') !==
329
+ b.vParameters.vParameters.map((x) => x.type).join(','))
330
+ return false;
331
+ if (a.visibility !== b.visibility)
332
+ return false;
333
+ return a.stateMutability === b.stateMutability;
334
+ };
335
+ exports.signatureEquals = signatureEquals;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recon-generate",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -22,7 +22,10 @@
22
22
  "abi-to-mock": "^1.0.4",
23
23
  "case": "^1.6.3",
24
24
  "commander": "^14.0.2",
25
- "handlebars": "^4.7.8"
25
+ "handlebars": "^4.7.8",
26
+ "solc-typed-ast": "^19.1.0",
27
+ "src-location": "^1.1.0",
28
+ "yaml": "^2.8.2"
26
29
  },
27
30
  "devDependencies": {
28
31
  "@types/node": "^24.0.4",