UNPKG

solidity-docgen

Version:

Solidity API documentation automatic generator.

255 lines (216 loc) 7.08 kB
import { fromPairs, pick } from 'lodash'; import fs from 'fs'; import semver from 'semver'; import { Filter } from './filter'; export interface SolcOutput { sources: { [file: string]: { ast: ast.SourceUnit; }; }; errors: { severity: string; formattedMessage: string; }[]; } export namespace ast { export interface SourceUnit { nodeType: 'SourceUnit'; id: number; nodes: SourceItem[]; } export type SourceItem = ContractDefinition | ImportDirective; export interface ImportDirective { nodeType: 'ImportDirective'; id: number; sourceUnit: number; symbolAliases: { foreign: { name: string; }; local?: null | string; }[] } export interface ContractDefinition { nodeType: 'ContractDefinition'; id: number; name: string; documentation: string | null; nodes: ContractItem[]; linearizedBaseContracts: number[]; } export type ContractItem = VariableDeclaration | FunctionDefinition | EventDefinition | ModifierDefinition; export interface VariableDeclaration { nodeType: 'VariableDeclaration'; visibility: 'internal' | 'public' | 'private'; name: string; constant: boolean; typeName: TypeName; } export interface FunctionDefinition { nodeType: 'FunctionDefinition'; kind: 'function' | 'constructor' | 'fallback'; visibility: 'internal' | 'external' | 'public' | 'private'; name: string; documentation: string | null; parameters: ParameterList; returnParameters: ParameterList; } export interface EventDefinition { nodeType: 'EventDefinition'; name: string; documentation: string | null; parameters: ParameterList; } export interface ModifierDefinition { nodeType: 'ModifierDefinition'; name: string; documentation: string | null; parameters: ParameterList; } export interface ParameterList { parameters: { name: string; typeName: TypeName; }[]; } export interface TypeName { nodeType: 'ElementaryTypeName' | 'UserDefinedTypeName'; typeDescriptions: { typeString: string; }; } } export const outputSelection = { '*': { '': [ 'ast', ], }, }; export async function compile( filter: Filter, solcModule: string = require.resolve('solc'), solcSettings: object = {optimizer: {enabled: true, runs: 200}}, ): Promise<SolcOutput> { const solc = await SolcAdapter.require(solcModule); const files = await filter.glob('*.sol'); const sources = fromPairs(await Promise.all(files.map(async file => [ file, { content: await fs.promises.readFile(file, 'utf8') }, ]))); const solcInput = { language: "Solidity", sources: sources, settings: { ...solcSettings, outputSelection }, }; const solcOutput = solc.compile(solcInput); const { errors: allErrors } = solcOutput; if (allErrors && allErrors.some(e => e.severity === 'error')) { const errors = allErrors.filter(e => e.severity === 'error'); const firstError = errors[0].formattedMessage; const moreErrors = errors.length === 1 ? '' : ` (And ${errors.length - 1} other errors...)`; throw new Error(`Solidity was unable to compile. ${firstError}${moreErrors}`); } return solcOutput; } export class SolcAdapter { static async require(solcModule: string): Promise<SolcAdapter> { const solc = await import(solcModule); return new SolcAdapter(solc); } constructor(private readonly solc: any) { } compile(input: object): SolcOutput { const inputJSON = JSON.stringify(input); const solcOutputString = semver.satisfies(this.solc.version(), '>=0.6') ? this.solc.compile(inputJSON, { import: importCallback }) : this.solc.compileStandardWrapper(inputJSON, importCallback); const solcOutput = JSON.parse(solcOutputString); if (semver.satisfies(this.solc.version(), '^0.4')) { for (const source of Object.values(solcOutput.sources) as any[]) { for (const fileNode of source.ast.nodes) { if (fileNode.nodeType === 'ContractDefinition') { for (const contractNode of fileNode.nodes) { if (contractNode.nodeType === 'FunctionDefinition') { if (contractNode.isConstructor) { contractNode.kind = 'constructor'; } else if (contractNode.name === '') { contractNode.kind = 'fallback'; } else { contractNode.kind = 'function'; } } } } } }; } const reader = new ASTReader(input, solcOutput); const adaptDocumentation = (node: any) => { if (typeof node.documentation === 'string') { // fix solc buggy parsing of doc comments // reverse engineered from solc behavior... node.documentation = cleanUpDocstringFromSolc(node.documentation); } else if (node.documentation?.text !== undefined) { const source = reader.read(node.documentation); if (source !== undefined) { node.documentation = cleanUpDocstringFromSource(source); } else { node.documentation = cleanUpDocstringFromSolc(node.documentation.text); } } }; for (const source of Object.values(solcOutput.sources) as any[]) { for (const fileNode of source.ast.nodes) { adaptDocumentation(fileNode); if (fileNode.nodeType === 'ContractDefinition') { for (const contractNode of fileNode.nodes) { adaptDocumentation(contractNode); } } } } return solcOutput; } } type SolidityImport = { contents: string } | { error: string }; function importCallback(path: string): SolidityImport { try { const resolved = require.resolve(path, { paths: ['.'] }); return { contents: fs.readFileSync(resolved, 'utf8'), }; } catch (e) { return { error: e.message, }; } } class ASTReader { constructor(private readonly input: any, private readonly output: any) {} read(node: { src: string }): string | undefined { const { source, start, length } = this.decodeSrc(node.src); return this.input.sources[source]?.content.slice(start, start + length); } private decodeSrc(src: string): { source: string; start: number; length: number } { const [start, length, sourceId] = src.split(':').map(s => parseInt(s)); const source = Object.keys(this.output.sources).find(s => this.output.sources[s].id === sourceId); if (source === undefined) { throw new Error(`No source with id ${sourceId}`); } return { source, start, length }; } } function cleanUpDocstringFromSolc(text: string) { // fix solc buggy parsing of doc comments // reverse engineered from solc behavior return text .replace(/\n\n?^[ \t]*(?:\*|\/\/\/)/mg, '\n\n') .replace(/^[ \t]?/mg, ''); } function cleanUpDocstringFromSource(text: string) { return text .replace(/^\/\*\*(.*)\*\/$/s, '$1') .trim() .replace(/^[ \t]*(\*|\/\/\/)[ \t]?/mg, ''); }