UNPKG

solidity-docgen

Version:

Documentation generator for Solidity smart contracts.

146 lines (126 loc) 4.69 kB
import { FunctionDefinition } from 'solidity-ast'; import { findAll } from 'solidity-ast/utils'; import { DocItemWithContext, DOC_ITEM_CONTEXT } from '../site'; import { arraysEqual } from './arrays-equal'; import { execAll } from './execall'; import { itemType } from './item-type'; import { ItemError } from './ItemError'; import { readItemDocs } from './read-item-docs'; import { getContractsInScope } from './scope'; export interface NatSpec { title?: string; notice?: string; dev?: string; params?: { name: string; description: string; }[]; returns?: { name?: string; description: string; }[]; custom?: { [tag: string]: string; }; } export function parseNatspec(item: DocItemWithContext): NatSpec { if (!item[DOC_ITEM_CONTEXT]) throw new Error(`Not an item or item is missing context`); let res: NatSpec = {}; const docSource = readItemDocs(item); const docString = docSource !== undefined ? cleanUpDocstringFromSource(docSource) : 'documentation' in item && item.documentation ? typeof item.documentation === 'string' ? item.documentation : cleanUpDocstringFromSolc(item.documentation.text) : ''; const tagMatches = execAll( /^(?:@(\w+|custom:[a-z][a-z-]*) )?((?:(?!^@(?:\w+|custom:[a-z][a-z-]*) )[^])*)/m, docString, ); let inheritFrom: FunctionDefinition | undefined; for (const [, tag = 'notice', content] of tagMatches) { if (content === undefined) throw new ItemError('Unexpected error', item); if (tag === 'dev' || tag === 'notice') { res[tag] ??= ''; res[tag] += content; } if (tag === 'title') { res.title = content.trim(); } if (tag === 'param') { const paramMatches = content.match(/(\w+) ([^]*)/); if (paramMatches) { const [, name, description] = paramMatches as [string, string, string]; res.params ??= []; res.params.push({ name, description: description.trim() }); } } if (tag === 'return') { if (!('returnParameters' in item)) { throw new ItemError(`Item does not contain return parameters`, item); } res.returns ??= []; const i = res.returns.length; const p = item.returnParameters.parameters[i]; if (p === undefined) { throw new ItemError('Got more @return tags than expected', item); } if (!p.name) { res.returns.push({ description: content.trim() }); } else { const paramMatches = content.match(/(\w+)( ([^]*))?/); if (!paramMatches || paramMatches[1] !== p.name) { throw new ItemError(`Expected @return tag to start with name '${p.name}'`, item); } const [, name, description] = paramMatches as [string, string, string?]; res.returns.push({ name, description: description?.trim() ?? '' }); } } if (tag?.startsWith('custom:')) { const key = tag.replace(/^custom:/, ''); res.custom ??= {}; res.custom[key] ??= ''; res.custom[key] += content; } if (tag === 'inheritdoc') { if (!(item.nodeType === 'FunctionDefinition' || item.nodeType === 'VariableDeclaration')) { throw new ItemError(`Expected function or variable but saw ${itemType(item)}`, item); } const parentContractName = content.trim(); const parentContract = getContractsInScope(item)[parentContractName]; if (!parentContract) { throw new ItemError(`Parent contract '${parentContractName}' not found`, item); } inheritFrom = [...findAll('FunctionDefinition', parentContract)].find(f => item.baseFunctions?.includes(f.id)); } } if (docString.length === 0) { if ('baseFunctions' in item && item.baseFunctions?.length === 1) { const baseFn = item[DOC_ITEM_CONTEXT].build.deref('FunctionDefinition', item.baseFunctions[0]!); const shouldInherit = item.nodeType === 'VariableDeclaration' || arraysEqual(item.parameters.parameters, baseFn.parameters.parameters, p => p.name); if (shouldInherit) { inheritFrom = baseFn; } } } if (res.dev) res.dev = res.dev.trim(); if (res.notice) res.notice = res.notice.trim(); if (inheritFrom) { res = { ...parseNatspec(inheritFrom as DocItemWithContext), ...res }; } return res; } // Fix solc buggy parsing of doc comments. // Reverse engineered from solc behavior. function cleanUpDocstringFromSolc(text: string) { 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, ''); }