import { parse } from "@solidity-parser/parser"; import { tool } from "langchain"; import { z } from "zod"; import { analyzeFunction, buildCommentBlocks, type ContractAnalysis, extractSolcVersion, findCommentFor, generateBriefMarkdown, generateFullMarkdown, paramToString, type RenderOptions, typeToString, } from "./utils.ts"; export const analyzeSolidityFile = async ( soliditySource: string, mode: "full" | "short", filePath?: string, importance?: number, ): Promise => { let ast: any; try { ast = parse(soliditySource, { tolerant: true, loc: true, range: true }); } catch (e: any) { return `# Parse Error\n\nFailed to parse Solidity source: ${e.message as string}`; } const comments = buildCommentBlocks(soliditySource); const imports: string[] = []; const contracts: ContractAnalysis[] = []; for (const node of ast.children ?? []) { if (node.type === "ImportDirective") imports.push(node.path as string); } for (const node of ast.children ?? []) { if (node.type !== "ContractDefinition") continue; const contractComment = node.loc ? findCommentFor(node.loc.start.line, comments) : undefined; const contract: ContractAnalysis = { name: node.name as string, kind: (node.kind as string) ?? "contract", baseContracts: (node.baseContracts ?? []).map( (bc: any) => (bc.baseName?.namePath ?? bc.baseName?.name ?? "?") as string, ), usingFor: [], stateVars: [], events: [], errors: [], modifiers: [], functions: [], natspec: contractComment?.natspec, }; const stateVarNames = new Set(); for (const member of node.subNodes ?? []) { const memberComment = member.loc ? findCommentFor(member.loc.start.line, comments) : undefined; switch (member.type) { case "StateVariableDeclaration": for (const v of member.variables ?? []) { stateVarNames.add(v.name as string); contract.stateVars.push({ name: v.name as string, type: typeToString(v.typeName), visibility: (v.visibility as string) ?? "internal", constant: (v.isDeclaredConst as boolean) ?? false, immutable: (v.isImmutable as boolean) ?? false, natspec: memberComment?.natspec, }); } break; case "EventDefinition": { const params = (member.parameters ?? []).map((p: any) => { const indexed = p.isIndexed ? "indexed " : ""; const name = p.name ? ` ${p.name as string}` : ""; return `${indexed}${typeToString(p.typeName)}${name}`; }); contract.events.push({ name: member.name as string, params, anonymous: (member.isAnonymous as boolean) ?? false, natspec: memberComment?.natspec, }); break; } case "CustomErrorDefinition": contract.errors.push({ name: member.name as string, params: (member.parameters ?? []).map((p: any) => paramToString(p)), natspec: memberComment?.natspec, }); break; case "ModifierDefinition": contract.modifiers.push({ name: member.name as string, params: (member.parameters ?? []).map(paramToString), natspec: memberComment?.natspec, }); break; case "FunctionDefinition": { const { internalCalls, externalCalls, stateReads, stateWrites } = analyzeFunction(member, stateVarNames); contract.functions.push({ name: (member.name as string) ?? "", isConstructor: (member.isConstructor as boolean) ?? false, isReceive: (member.isReceiveEther as boolean) ?? false, isFallback: (member.isFallback as boolean) ?? false, visibility: (member.visibility as string) ?? "internal", mutability: (member.stateMutability as string) ?? "nonpayable", params: (member.parameters ?? []).map(paramToString), returns: (member.returnParameters ?? []).map(paramToString), modifiers: (member.modifiers ?? []).map((m: any) => m.name as string), internalCalls, externalCalls, stateReads, stateWrites, natspec: memberComment?.natspec, }); break; } case "UsingForDeclaration": { const forType = member.typeName ? typeToString(member.typeName) : "*"; if (member.libraryName) { contract.usingFor.push(`\`${member.libraryName as string}\` for \`${forType}\``); } else { const fns = (member.functions ?? []) .map((f: any) => (f.typeName?.namePath ?? f.typeName?.name ?? f.path ?? "?") as string) .join(", "); contract.usingFor.push(`{${fns}} for \`${forType}\``); } break; } } } contracts.push(contract); } const opts: RenderOptions = { filePath, importance, lineCount: soliditySource.split("\n").length, solcVersion: extractSolcVersion(soliditySource), }; return mode === "short" ? generateBriefMarkdown(imports, contracts, opts) : generateFullMarkdown(imports, contracts, opts); }; export const solidityAnalyzerTool = tool(async ({ solidityFile, mode }) => analyzeSolidityFile(solidityFile, mode), { name: "solidity_analyzer", description: "Parse a Solidity source file and generate a markdown report. Use mode='short' for a compact summary (meta, external calls, function table). Use mode='full' for the complete report including storage, events, errors, per-function call graph, recursion detection, and state variable touchpoints.", schema: z.object({ solidityFile: z.string().describe("The full Solidity source code to analyze."), mode: z .enum(["full", "short"]) .default("full") .describe("Report verbosity. 'short' saves tokens; 'full' provides the complete analysis."), }), });