solidity-docgen
Version:
Documentation generator for Solidity smart contracts.
146 lines (126 loc) • 4.69 kB
text/typescript
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, '');
}