recon-generate 0.0.5 → 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;
@@ -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
@@ -117,6 +117,7 @@ async function main() {
117
117
  .option('--mock <names>', 'Comma-separated contract names to generate mocks for')
118
118
  .option('--dynamic-deploy <names>', 'Comma-separated contract names to enable dynamic deploy lists')
119
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)')
120
121
  .option('--list', 'List available contracts/functions (after filters) and exit')
121
122
  .option('--force', 'Replace existing generated suite output (under --output). Does not rebuild .recon/out.')
122
123
  .option('--force-build', 'Delete .recon/out to force a fresh forge build before generating')
@@ -193,6 +194,7 @@ async function main() {
193
194
  dynamicDeploy: dynamicDeploySet,
194
195
  suiteNameSnake: suiteSnake,
195
196
  suiteNamePascal: suitePascal,
197
+ coverage: !!opts.coverage,
196
198
  });
197
199
  if (opts.list) {
198
200
  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,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.5",
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": {
@@ -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": {