recon-generate 0.0.5 → 0.0.7

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,22 @@ 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
+ # Generate coverage JSON from a Crytic tester (no scaffolding)
73
+ recon-generate coverage --crytic-name CryticTester --name foo
74
+
75
+ # Link subcommand
76
+
77
+ After generating a suite you can update Echidna and Medusa configs with detected Foundry libraries:
78
+
79
+ ```bash
80
+ recon-generate link
81
+ ```
82
+
83
+ This runs `crytic-compile` with `--foundry-compile-all` to find linked libraries and rewrites:
84
+
85
+ - Echidna YAML `cryticArgs` and `deployContracts` with deterministic placeholder addresses
86
+ - Medusa `compilation.platformConfig.args` to include `--compile-libraries`
70
87
  ```
71
88
 
72
89
  ### Behavior
@@ -0,0 +1 @@
1
+ export declare const runCoverage: (foundryRoot: string, foundryConfigPath: string, cryticName: string, suiteNameSnake?: string) => Promise<void>;
@@ -0,0 +1,221 @@
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.runCoverage = void 0;
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs/promises"));
39
+ const path = __importStar(require("path"));
40
+ const solc_typed_ast_1 = require("solc-typed-ast");
41
+ const processor_1 = require("./processor");
42
+ const types_1 = require("./types");
43
+ const utils_1 = require("./utils");
44
+ const isSetupName = (name) => {
45
+ if (!name)
46
+ return false;
47
+ return name.toLowerCase() === 'setup';
48
+ };
49
+ const stripDataLocation = (raw) => {
50
+ if (!raw)
51
+ return '';
52
+ const noLocation = raw
53
+ .replace(/\s+(storage|memory|calldata)(\s+pointer)?/g, '')
54
+ .replace(/\s+pointer/g, '');
55
+ const noPrefix = noLocation
56
+ .replace(/^struct\s+/, '')
57
+ .replace(/^enum\s+/, '')
58
+ .replace(/^contract\s+/, '')
59
+ .trim();
60
+ return noPrefix;
61
+ };
62
+ const signatureFromFnDef = (fnDef) => {
63
+ const params = fnDef.vParameters.vParameters
64
+ .map((p) => { var _a; return stripDataLocation((_a = p.typeString) !== null && _a !== void 0 ? _a : ''); })
65
+ .join(',');
66
+ const name = fnDef.name
67
+ || (fnDef.kind === solc_typed_ast_1.FunctionKind.Constructor
68
+ ? 'constructor'
69
+ : fnDef.kind === solc_typed_ast_1.FunctionKind.Fallback
70
+ ? 'fallback'
71
+ : fnDef.kind === solc_typed_ast_1.FunctionKind.Receive
72
+ ? 'receive'
73
+ : '');
74
+ return `${name}(${params})`;
75
+ };
76
+ const runCmd = (cmd, cwd) => {
77
+ return new Promise((resolve, reject) => {
78
+ (0, child_process_1.exec)(cmd, { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (err, _stdout, stderr) => {
79
+ if (err) {
80
+ reject(new Error(stderr || err.message));
81
+ }
82
+ else {
83
+ resolve();
84
+ }
85
+ });
86
+ });
87
+ };
88
+ const loadLatestSourceUnits = async (foundryRoot) => {
89
+ var _a;
90
+ const outDir = path.join(foundryRoot, '.recon', 'out');
91
+ const buildInfoDir = path.join(outDir, 'build-info');
92
+ let files = [];
93
+ try {
94
+ const entries = await fs.readdir(buildInfoDir);
95
+ const jsonFiles = entries.filter((f) => f.endsWith('.json'));
96
+ files = await Promise.all(jsonFiles.map(async (f) => ({
97
+ name: f,
98
+ path: path.join(buildInfoDir, f),
99
+ mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime,
100
+ })));
101
+ }
102
+ catch (e) {
103
+ throw new Error(`No build-info directory found at ${buildInfoDir}: ${e}`);
104
+ }
105
+ if (files.length === 0) {
106
+ throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
107
+ }
108
+ const latestFile = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path;
109
+ const fileContent = await fs.readFile(latestFile, 'utf-8');
110
+ const buildInfo = JSON.parse(fileContent);
111
+ const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
112
+ if (!buildOutput) {
113
+ throw new Error(`Build-info file ${latestFile} is missing output data.`);
114
+ }
115
+ const filteredAstData = { ...buildOutput };
116
+ if (filteredAstData.sources) {
117
+ const validSources = {};
118
+ for (const [key, content] of Object.entries(filteredAstData.sources)) {
119
+ const ast = content.ast || content.legacyAST || content.AST;
120
+ if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
121
+ validSources[key] = content;
122
+ }
123
+ }
124
+ filteredAstData.sources = validSources;
125
+ }
126
+ const reader = new solc_typed_ast_1.ASTReader();
127
+ return reader.read(filteredAstData);
128
+ };
129
+ const shouldIncludeFunction = (fnDef) => {
130
+ if (!fnDef.implemented)
131
+ return false;
132
+ if (isSetupName(fnDef.name))
133
+ return false;
134
+ if (fnDef.kind !== solc_typed_ast_1.FunctionKind.Function)
135
+ return false;
136
+ if (fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.External &&
137
+ fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.Public) {
138
+ return false;
139
+ }
140
+ if (fnDef.stateMutability === solc_typed_ast_1.FunctionStateMutability.View ||
141
+ fnDef.stateMutability === solc_typed_ast_1.FunctionStateMutability.Pure) {
142
+ return false;
143
+ }
144
+ return true;
145
+ };
146
+ const addFunction = (map, contractName, fnDef) => {
147
+ var _a;
148
+ const sig = signatureFromFnDef(fnDef);
149
+ if (!sig)
150
+ return;
151
+ const existing = (_a = map.get(contractName)) !== null && _a !== void 0 ? _a : new Set();
152
+ existing.add(sig);
153
+ map.set(contractName, existing);
154
+ };
155
+ const collectContractFunctions = (sourceUnits, cryticName) => {
156
+ var _a;
157
+ const functionsByContract = new Map();
158
+ const contracts = [];
159
+ for (const unit of sourceUnits) {
160
+ for (const contract of unit.getChildrenByType(solc_typed_ast_1.ContractDefinition)) {
161
+ if (contract.name === cryticName) {
162
+ contracts.push(contract);
163
+ }
164
+ }
165
+ }
166
+ if (contracts.length === 0) {
167
+ throw new Error(`Contract ${cryticName} not found in build output.`);
168
+ }
169
+ const contract = contracts[0];
170
+ const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
171
+ for (const fnDef of allFunctions) {
172
+ if (!shouldIncludeFunction(fnDef)) {
173
+ continue;
174
+ }
175
+ addFunction(functionsByContract, contract.name, fnDef);
176
+ for (const call of fnDef.getChildrenByType(solc_typed_ast_1.FunctionCall)) {
177
+ const callType = (0, utils_1.getCallType)(call);
178
+ if (callType === types_1.CallType.Internal) {
179
+ continue;
180
+ }
181
+ const ref = (_a = call.vExpression.vReferencedDeclaration) !== null && _a !== void 0 ? _a : call.vReferencedDeclaration;
182
+ if (!(ref instanceof solc_typed_ast_1.FunctionDefinition)) {
183
+ continue;
184
+ }
185
+ const targetContract = ref.getClosestParentByType(solc_typed_ast_1.ContractDefinition);
186
+ if (!targetContract) {
187
+ continue;
188
+ }
189
+ if (ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.View ||
190
+ ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.Pure) {
191
+ continue;
192
+ }
193
+ if (ref.kind === solc_typed_ast_1.FunctionKind.Constructor ||
194
+ ref.kind === solc_typed_ast_1.FunctionKind.Fallback ||
195
+ ref.kind === solc_typed_ast_1.FunctionKind.Receive) {
196
+ continue;
197
+ }
198
+ addFunction(functionsByContract, targetContract.name, ref);
199
+ }
200
+ }
201
+ return functionsByContract;
202
+ };
203
+ const runCoverage = async (foundryRoot, foundryConfigPath, cryticName, suiteNameSnake) => {
204
+ const outDir = path.join(foundryRoot, '.recon', 'out');
205
+ const buildCmd = `forge build --contracts ${cryticName} --build-info --out .recon/out`.replace(/\s+/g, ' ').trim();
206
+ await runCmd(buildCmd, foundryRoot);
207
+ const sourceUnits = await loadLatestSourceUnits(foundryRoot);
208
+ if (!sourceUnits || sourceUnits.length === 0) {
209
+ throw new Error('No source units were produced from the Crytic build; cannot generate coverage.');
210
+ }
211
+ const contractFunctions = collectContractFunctions(sourceUnits, cryticName);
212
+ if (contractFunctions.size === 0) {
213
+ throw new Error('No eligible functions found to include in coverage.');
214
+ }
215
+ const coverage = await (0, processor_1.buildCoverageMap)(sourceUnits, foundryRoot, contractFunctions);
216
+ const coverageName = suiteNameSnake ? `recon-${suiteNameSnake}-coverage.json` : 'recon-coverage.json';
217
+ const coveragePath = path.join(foundryRoot, coverageName);
218
+ await fs.writeFile(coveragePath, JSON.stringify(coverage, null, 2));
219
+ console.log(`[recon-generate] Wrote coverage file to ${coveragePath}`);
220
+ };
221
+ exports.runCoverage = runCoverage;
@@ -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;
@@ -109,14 +111,14 @@ class ReconGenerator {
109
111
  if (await (0, utils_1.fileExists)(chimeraPath)) {
110
112
  return;
111
113
  }
112
- await this.runCmd('forge install Recon-Fuzz/chimera --no-git', this.foundryRoot);
114
+ await this.runCmd('forge install Recon-Fuzz/chimera', this.foundryRoot);
113
115
  }
114
116
  async installSetupHelpers() {
115
117
  const setupPath = path.join(this.foundryRoot, 'lib', 'setup-helpers');
116
118
  if (await (0, utils_1.fileExists)(setupPath)) {
117
119
  return;
118
120
  }
119
- await this.runCmd('forge install Recon-Fuzz/setup-helpers --no-git', this.foundryRoot);
121
+ await this.runCmd('forge install Recon-Fuzz/setup-helpers', this.foundryRoot);
120
122
  }
121
123
  async updateRemappings() {
122
124
  const remappingsPath = path.join(this.foundryRoot, 'remappings.txt');
@@ -472,7 +474,7 @@ class ReconGenerator {
472
474
  return config;
473
475
  }
474
476
  async run() {
475
- var _a, _b;
477
+ var _a, _b, _c, _d, _e;
476
478
  await this.ensureFoundryConfigExists();
477
479
  const outPath = this.outDir();
478
480
  const mockTargets = (_a = this.options.mocks) !== null && _a !== void 0 ? _a : new Set();
@@ -493,6 +495,73 @@ class ReconGenerator {
493
495
  }
494
496
  await this.ensureBuild(); // first build to get ABIs
495
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
+ }
496
565
  // Generate mocks if requested, then rebuild to pick them up
497
566
  let tempMockSrc = null;
498
567
  if (mockTargets.size > 0) {
@@ -511,12 +580,63 @@ class ReconGenerator {
511
580
  await this.copyAndCleanupMocks(tempMockSrc);
512
581
  }
513
582
  const contracts = await this.findSourceContracts();
514
- const filteredContracts = contracts.filter(c => this.isContractAllowed(c.name));
515
- if (filteredContracts.length === 0) {
516
- 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
+ }
517
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 });
518
605
  const reconConfig = await this.loadReconConfig(filteredContracts);
519
- 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());
520
640
  await tm.generateTemplates(filteredContracts, reconConfig);
521
641
  }
522
642
  async listAvailable() {
@@ -541,16 +661,16 @@ class ReconGenerator {
541
661
  if (fns.length > 0) {
542
662
  filtered.push({ name: contract.name, path: contract.path, functions: fns });
543
663
  }
544
- }
545
- console.log('Available contracts and functions (after filters):');
546
- for (const c of filtered) {
547
- console.log(`- ${c.name} :: ${c.path}`);
548
- for (const fn of c.functions) {
549
- 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.');
550
673
  }
551
- }
552
- if (filtered.length === 0) {
553
- console.log('No contracts/functions match the current include/exclude filters.');
554
674
  }
555
675
  }
556
676
  }
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ const commander_1 = require("commander");
38
38
  const path = __importStar(require("path"));
39
39
  const case_1 = require("case");
40
40
  const generator_1 = require("./generator");
41
+ const coverage_1 = require("./coverage");
41
42
  const utils_1 = require("./utils");
42
43
  const link_1 = require("./link");
43
44
  function parseFilter(input) {
@@ -117,10 +118,25 @@ async function main() {
117
118
  .option('--mock <names>', 'Comma-separated contract names to generate mocks for')
118
119
  .option('--dynamic-deploy <names>', 'Comma-separated contract names to enable dynamic deploy lists')
119
120
  .option('--debug', 'Print debug info about filters and selection')
121
+ .option('--coverage', 'Write coverage information to recon-coverage.json (or recon-<name>-coverage.json)')
120
122
  .option('--list', 'List available contracts/functions (after filters) and exit')
121
123
  .option('--force', 'Replace existing generated suite output (under --output). Does not rebuild .recon/out.')
122
124
  .option('--force-build', 'Delete .recon/out to force a fresh forge build before generating')
123
125
  .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)');
126
+ program
127
+ .command('coverage')
128
+ .description('Generate recon-coverage.json from a Crytic tester contract without scaffolding tests')
129
+ .option('--crytic-name <name>', 'Name of the Crytic tester contract to compile', 'CryticTester')
130
+ .option('--name <suite>', 'Suite name; affects coverage filename (recon-<name>-coverage.json)')
131
+ .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
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
+ await (0, coverage_1.runCoverage)(foundryRoot, foundryConfig, opts.cryticName || 'CryticTester', suiteSnake);
139
+ });
124
140
  program
125
141
  .command('link')
126
142
  .description('Link library addresses into echidna/medusa configs via crytic-compile')
@@ -193,6 +209,7 @@ async function main() {
193
209
  dynamicDeploy: dynamicDeploySet,
194
210
  suiteNameSnake: suiteSnake,
195
211
  suiteNamePascal: suitePascal,
212
+ coverage: !!opts.coverage,
196
213
  });
197
214
  if (opts.list) {
198
215
  await generator.listAvailable();
@@ -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,252 @@
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
+ const skipPatterns = [
157
+ 'test/',
158
+ 'tests/',
159
+ 'script/',
160
+ 'scripts/',
161
+ 'contracts/test/',
162
+ 'contracts/script/',
163
+ 'contracts/scripts/',
164
+ 'lib/forge-std/',
165
+ 'lib/chimera/',
166
+ 'lib/setup-helpers/',
167
+ ];
168
+ async function buildCoverageMap(asts, foundryRoot, contractFunctions) {
169
+ const coverage = new Map();
170
+ const fileCache = new Map();
171
+ let nodesVisited = 0;
172
+ let nodesAdded = 0;
173
+ const shouldSkipPath = (relPath) => {
174
+ return skipPatterns.some((p) => relPath.includes(p));
175
+ };
176
+ const addNodeRange = async (node) => {
177
+ var _a;
178
+ nodesVisited++;
179
+ const srcUnit = node.getClosestParentByType($.SourceUnit);
180
+ if (!srcUnit) {
181
+ return;
182
+ }
183
+ const parentContract = node.getClosestParentByType($.ContractDefinition);
184
+ if (parentContract && parentContract.kind === 'interface') {
185
+ return;
186
+ }
187
+ const absPath = path.isAbsolute(srcUnit.absolutePath)
188
+ ? srcUnit.absolutePath
189
+ : path.join(foundryRoot, srcUnit.absolutePath);
190
+ let content = fileCache.get(absPath);
191
+ if (!content) {
192
+ try {
193
+ content = await fs.readFile(absPath, 'utf8');
194
+ fileCache.set(absPath, content);
195
+ }
196
+ catch {
197
+ return;
198
+ }
199
+ }
200
+ const src = node.src;
201
+ if (!src) {
202
+ return;
203
+ }
204
+ const { start, end } = (0, utils_1.getLines)(content, src);
205
+ const key = path.relative(foundryRoot, absPath);
206
+ const entry = (_a = coverage.get(key)) !== null && _a !== void 0 ? _a : new Set();
207
+ entry.add(start === end ? `${start}` : `${start}-${end}`);
208
+ coverage.set(key, entry);
209
+ nodesAdded++;
210
+ };
211
+ const walkRecord = async (rec) => {
212
+ await addNodeRange(rec.ast);
213
+ for (const child of rec.children) {
214
+ await walkRecord(child);
215
+ }
216
+ };
217
+ for (const ast of asts) {
218
+ for (const contract of ast.getChildrenByType($.ContractDefinition)) {
219
+ if (contract.kind === 'interface') {
220
+ continue;
221
+ }
222
+ const allowedFns = contractFunctions.get(contract.name);
223
+ if (!allowedFns || allowedFns.size === 0) {
224
+ continue;
225
+ }
226
+ const processed = (0, exports.processContract)(contract);
227
+ const recs = (processed['vFunctions'] || []);
228
+ if (recs.length === 0) {
229
+ continue;
230
+ }
231
+ for (const rec of recs) {
232
+ const sig = signatureFromFnDef(rec.ast);
233
+ if (!matchesSignature(sig, allowedFns)) {
234
+ continue;
235
+ }
236
+ await walkRecord(rec);
237
+ }
238
+ }
239
+ }
240
+ const normalized = {};
241
+ for (const [relPath, ranges] of coverage.entries()) {
242
+ if (shouldSkipPath(relPath)) {
243
+ continue; // skip test/script/lib files at the final emission step only
244
+ }
245
+ const sorted = Array.from(ranges).sort((a, b) => {
246
+ const start = (val) => parseInt(val.split('-')[0], 10);
247
+ return start(a) - start(b);
248
+ });
249
+ normalized[relPath] = sorted;
250
+ }
251
+ return normalized;
252
+ }
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,227 @@ 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
+ const visited = new Set();
288
+ const gather = (c) => {
289
+ if (visited.has(c)) {
290
+ return [];
291
+ }
292
+ visited.add(c);
293
+ const own = inclusion ? c[kind] : [];
294
+ const defs = [...(own || [])];
295
+ for (const base of c.vLinearizedBaseContracts) {
296
+ if (base === c) {
297
+ continue;
298
+ }
299
+ for (const def of gather(base)) {
300
+ if (!defs.includes(def)) {
301
+ defs.push(def);
302
+ }
303
+ }
304
+ }
305
+ return defs;
306
+ };
307
+ return gather(contract);
308
+ }
309
+ function toSource(node, version) {
310
+ const formatter = new solc_typed_ast_1.PrettyFormatter(4, 0);
311
+ const writer = new solc_typed_ast_1.ASTWriter(solc_typed_ast_1.DefaultASTWriterMapping, formatter, version ? version : solc_typed_ast_1.LatestCompilerVersion);
312
+ return writer.write(node);
313
+ }
314
+ function getCallType(fnCall) {
315
+ if (highLevelCall(fnCall)) {
316
+ return types_1.CallType.HighLevel;
317
+ }
318
+ else if (lowLevelCall(fnCall)) {
319
+ return types_1.CallType.LowLevel;
320
+ }
321
+ return types_1.CallType.Internal;
322
+ }
323
+ const getFunctionName = (fnDef) => {
324
+ if (fnDef.name) {
325
+ return fnDef.name;
326
+ }
327
+ if (fnDef.isConstructor || fnDef.kind === solc_typed_ast_1.FunctionKind.Constructor) {
328
+ return 'constructor';
329
+ }
330
+ if (fnDef.kind === solc_typed_ast_1.FunctionKind.Fallback) {
331
+ return 'fallback';
332
+ }
333
+ if (fnDef.kind === solc_typed_ast_1.FunctionKind.Receive) {
334
+ return 'receive';
335
+ }
336
+ return 'Unknown';
337
+ };
338
+ exports.getFunctionName = getFunctionName;
339
+ const signatureEquals = (a, b) => {
340
+ if (!a.name || !b.name)
341
+ return false;
342
+ if (a.name !== b.name)
343
+ return false;
344
+ if (a.vParameters.vParameters.map((x) => x.type).join(',') !==
345
+ b.vParameters.vParameters.map((x) => x.type).join(','))
346
+ return false;
347
+ if (a.visibility !== b.visibility)
348
+ return false;
349
+ return a.stateMutability === b.stateMutability;
350
+ };
351
+ exports.signatureEquals = signatureEquals;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recon-generate",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -23,6 +23,8 @@
23
23
  "case": "^1.6.3",
24
24
  "commander": "^14.0.2",
25
25
  "handlebars": "^4.7.8",
26
+ "solc-typed-ast": "^19.1.0",
27
+ "src-location": "^1.1.0",
26
28
  "yaml": "^2.8.2"
27
29
  },
28
30
  "devDependencies": {