recon-generate 0.0.6 → 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
@@ -69,6 +69,9 @@ recon-generate --mock "Morpho,Other" --force
69
69
  # Enable dynamic deploy helpers for Foo and Bar
70
70
  recon-generate --dynamic-deploy "Foo,Bar" --force
71
71
 
72
+ # Generate coverage JSON from a Crytic tester (no scaffolding)
73
+ recon-generate coverage --crytic-name CryticTester --name foo
74
+
72
75
  # Link subcommand
73
76
 
74
77
  After generating a suite you can update Echidna and Medusa configs with detected Foundry libraries:
@@ -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;
package/dist/generator.js CHANGED
@@ -111,14 +111,14 @@ class ReconGenerator {
111
111
  if (await (0, utils_1.fileExists)(chimeraPath)) {
112
112
  return;
113
113
  }
114
- await this.runCmd('forge install Recon-Fuzz/chimera --no-git', this.foundryRoot);
114
+ await this.runCmd('forge install Recon-Fuzz/chimera', this.foundryRoot);
115
115
  }
116
116
  async installSetupHelpers() {
117
117
  const setupPath = path.join(this.foundryRoot, 'lib', 'setup-helpers');
118
118
  if (await (0, utils_1.fileExists)(setupPath)) {
119
119
  return;
120
120
  }
121
- 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);
122
122
  }
123
123
  async updateRemappings() {
124
124
  const remappingsPath = path.join(this.foundryRoot, 'remappings.txt');
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) {
@@ -122,6 +123,20 @@ async function main() {
122
123
  .option('--force', 'Replace existing generated suite output (under --output). Does not rebuild .recon/out.')
123
124
  .option('--force-build', 'Delete .recon/out to force a fresh forge build before generating')
124
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
+ });
125
140
  program
126
141
  .command('link')
127
142
  .description('Link library addresses into echidna/medusa configs via crytic-compile')
package/dist/processor.js CHANGED
@@ -153,11 +153,26 @@ const processContract = (contract) => {
153
153
  return result;
154
154
  };
155
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
+ ];
156
168
  async function buildCoverageMap(asts, foundryRoot, contractFunctions) {
157
169
  const coverage = new Map();
158
170
  const fileCache = new Map();
159
171
  let nodesVisited = 0;
160
172
  let nodesAdded = 0;
173
+ const shouldSkipPath = (relPath) => {
174
+ return skipPatterns.some((p) => relPath.includes(p));
175
+ };
161
176
  const addNodeRange = async (node) => {
162
177
  var _a;
163
178
  nodesVisited++;
@@ -224,6 +239,9 @@ async function buildCoverageMap(asts, foundryRoot, contractFunctions) {
224
239
  }
225
240
  const normalized = {};
226
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
+ }
227
245
  const sorted = Array.from(ranges).sort((a, b) => {
228
246
  const start = (val) => parseInt(val.split('-')[0], 10);
229
247
  return start(a) - start(b);
package/dist/utils.js CHANGED
@@ -284,11 +284,27 @@ function getDeepRef(node) {
284
284
  }
285
285
  }
286
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;
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);
292
308
  }
293
309
  function toSource(node, version) {
294
310
  const formatter = new solc_typed_ast_1.PrettyFormatter(4, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recon-generate",
3
- "version": "0.0.6",
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": {