UNPKG

@solarity/hardhat-markup

Version:

Customizable markdown smart contracts documentation

440 lines 21.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Parser = void 0; const utils_1 = require("solidity-ast/utils"); const constants_1 = require("./constants"); const prettier_plugin_solidity_1 = __importDefault(require("prettier-plugin-solidity")); const prettier = require("prettier"); class Parser { constructor(contractBuildInfo) { this.contractBuildInfo = contractBuildInfo; this.deref = (0, utils_1.astDereferencer)(contractBuildInfo.output); } async parseContractInfo(source, name) { const sourceUnit = this.contractBuildInfo.output.sources[source].ast; const contractNode = sourceUnit.nodes.find((node) => (0, utils_1.isNodeType)("ContractDefinition", node) && node.name === name); if (!contractNode) { throw new Error(`Contract ${name} not found in ${source}`); } const allFunctions = [...(0, utils_1.findAll)("FunctionDefinition", contractNode)]; let functions; if (contractNode.contractKind === "library" && allFunctions.every((fn) => this.isPrivateOrInternal(fn))) { functions = allFunctions.filter((node) => this.isInternal(node)); } else { functions = allFunctions.filter((node) => this.isPublicOrExternal(node)); } return { name: contractNode.name, isAbstract: contractNode.abstract, contractKind: contractNode.contractKind, license: this.parseLicense(sourceUnit), documentations: await Promise.all([ this.parseDocumentation([contractNode], ""), this.parseDocumentation([...(0, utils_1.findAll)("EnumDefinition", contractNode)], constants_1.ENUMS_BLOCK_NAME), this.parseDocumentation([...(0, utils_1.findAll)("StructDefinition", contractNode)], constants_1.STRUCTS_BLOCK_NAME), this.parseDocumentation([...(0, utils_1.findAll)("EventDefinition", contractNode)], constants_1.EVENTS_BLOCK_NAME), this.parseDocumentation([...(0, utils_1.findAll)("ErrorDefinition", contractNode)], constants_1.ERRORS_BLOCK_NAME), this.parseDocumentation([...(0, utils_1.findAll)("VariableDeclaration", contractNode)].filter((node) => node.constant && this.isPublicOrExternal(node)), constants_1.CONSTANTS_BLOCK_NAME), this.parseDocumentation([...(0, utils_1.findAll)("VariableDeclaration", contractNode)].filter((node) => !node.constant && this.isPublicOrExternal(node)), constants_1.STATE_VARIABLES_BLOCK_NAME), this.parseDocumentation([...(0, utils_1.findAll)("ModifierDefinition", contractNode)], constants_1.MODIFIERS_BLOCK_NAME), this.parseDocumentation(functions, constants_1.FUNCTIONS_BLOCK_NAME), ]), }; } isInternal(node) { return node.visibility === "internal"; } isPrivateOrInternal(node) { return node.visibility === "private" || this.isInternal(node); } isPublicOrExternal(node) { return node.visibility === "public" || node.visibility === "external"; } parseSelector(node) { return node.functionSelector ? ` (0x${node.functionSelector})` : ""; } async applyPrettier(text) { return (await prettier.format(text, { parser: "solidity-parse", plugins: [prettier_plugin_solidity_1.default] })).trim(); } parseHeader(node) { switch (node.nodeType) { case "FunctionDefinition": { return `${node.name ? node.name : node.kind}${this.parseSelector(node)}`; } case "VariableDeclaration": { return `${node.name}${this.parseSelector(node)}`; } case "ContractDefinition": { return ""; } default: { return node.name; } } } async parseFullSign(node) { switch (node.nodeType) { case "FunctionDefinition": { return this.parseFullFunctionSign(node); } case "VariableDeclaration": { return this.parseFullStateVariableSign(node); } case "EventDefinition": { return this.parseFullEventSign(node); } case "ErrorDefinition": { return this.parseFullErrorSign(node); } case "EnumDefinition": { return this.parseFullEnumSign(node); } case "StructDefinition": { return this.parseFullStructSign(node); } case "ModifierDefinition": { return this.parseFullModifierSign(node); } case "ContractDefinition": { return this.parseFullContractSign(node); } default: { throw new Error(`Unknown node type ${node.nodeType}`); } } } async parseDocumentation(nodes, name) { return { blockName: name, documentation: await Promise.all(nodes.map(async (node) => ({ fullSign: await this.parseFullSign(node), header: this.parseHeader(node), natSpecDocumentation: this.parseNatSpecDocumentation(node), }))), }; } removeTypePrefix(typeString) { if (typeString.includes("enum ")) { return typeString.replace("enum ", ""); } else if (typeString.includes("struct ")) { return typeString.replace("struct ", ""); } else if (typeString.includes("contract ")) { return typeString.replace("contract ", ""); } return typeString; } buildParameterString(parameters, delimiter = ", ", beginning = "", ending = "") { return parameters .map((variableDeclaration) => { return `${beginning}${this.removeTypePrefix(variableDeclaration.typeDescriptions.typeString || "")}${variableDeclaration.storageLocation === "default" ? "" : ` ${variableDeclaration.storageLocation}`}${variableDeclaration.indexed ? " indexed" : ""}${variableDeclaration.name ? ` ${variableDeclaration.name}` : ""}${ending}`; }) .join(delimiter); } parseModifiersString(modifiers) { return modifiers .map((modifier) => modifier.modifierName.name + (modifier.arguments ? `(${modifier.arguments .map((expression) => { // EXPERIMENTAL return this.parseStringFromSourceCode(expression.src); }) .join(", ")})` : "")) .join(" "); } async parseFullFunctionSign(functionDefinition) { const kind = functionDefinition.kind; const functionName = functionDefinition.name.length === 0 ? "" : ` ${functionDefinition.name}`; const parameters = this.buildParameterString(functionDefinition.parameters.parameters); const visibility = kind === "constructor" ? "" : ` ${functionDefinition.visibility}`; const stateMutability = functionDefinition.stateMutability === "nonpayable" ? "" : ` ${functionDefinition.stateMutability}`; const modifiers = functionDefinition.modifiers.length === 0 ? "" : ` ${this.parseModifiersString(functionDefinition.modifiers)}`; const virtual = functionDefinition.virtual ? " virtual" : ""; const overrides = functionDefinition.overrides ? " override" : ""; const returns = functionDefinition.returnParameters.parameters.length === 0 ? "" : ` returns (${this.buildParameterString(functionDefinition.returnParameters.parameters)})`; const formattedRes = await this.applyPrettier(`${kind}${functionName}(${parameters})${visibility}${stateMutability}${modifiers}${virtual}${overrides}${returns};`); return formattedRes.substring(0, formattedRes.length - 1); } parseStringFromSourceCode(src) { const [start, end, id] = src.split(":").map((x) => parseInt(x)); const sourceFile = this.contractBuildInfo.input.sources[Object.values(this.contractBuildInfo.output.sources).find((source) => source.id === id)?.ast.absolutePath].content; return Buffer.from(sourceFile, "utf-8") .subarray(start, start + end) .toString(); } parseFullStateVariableSign(stateVariable) { let res = `${stateVariable.typeDescriptions.typeString}${stateVariable.mutability === "mutable" ? "" : ` ${stateVariable.mutability}`} ${stateVariable.name}`; // EXPERIMENTAL if (stateVariable.value) { res += ` = ${this.parseStringFromSourceCode(stateVariable.value.src)}`; } return res; } parseFullEventSign(eventDefinition) { return `event ${eventDefinition.name}(${this.buildParameterString(eventDefinition.parameters.parameters)})${eventDefinition.anonymous ? " anonymous" : ""}`; } parseFullErrorSign(errorDefinition) { return `error ${errorDefinition.name}(${this.buildParameterString(errorDefinition.parameters.parameters)})`; } parseFullStructSign(structDefinition) { return `struct ${structDefinition.name} {\n${this.buildParameterString(structDefinition.members, "\n", "\t", ";")}\n}`; } parseFullEnumSign(enumDefinition) { const parameters = enumDefinition.members.map((enumValue) => `\t ${enumValue.name}`).join(",\n"); return `enum ${enumDefinition.name} {\n${parameters}\n}`; } parseFullModifierSign(modifierDefinition) { return `modifier ${modifierDefinition.name}(${this.buildParameterString(modifierDefinition.parameters.parameters)})`; } parseFullContractSign(contractDefinition) { return `${contractDefinition.abstract ? "abstract " : ""}${contractDefinition.contractKind} ${contractDefinition.name}${contractDefinition.baseContracts.length === 0 ? "" : ` is ${contractDefinition.baseContracts.map((x) => x.baseName.name).join(", ")}`}`; } parseLicense(node) { return node.license || constants_1.DEFAULT_LICENSE; } deleteCommentSymbols(text) { let inCommentBlock = false; let startOfCommentBlock = false; let spacesToRemove = 0; return text .replace(/^\/\*\*([\s\S]*?)\*\/$/m, "$1") .trim() .replace(/^[ \t]*((\*{1,2}|\/{2,3}))+([ \t]*)(.*\n?)/gm, (match, p1, p2, p3, p4) => { if (p4.includes("```")) { inCommentBlock = !inCommentBlock; startOfCommentBlock = true; return p4; } if (inCommentBlock) { if (startOfCommentBlock) { spacesToRemove = p3.length; startOfCommentBlock = false; } return p3.substring(spacesToRemove) + p4; } return p4; }) .trim(); } replaceMultipleNewLinesWithOne(text) { return text.replace(/\n{3,}/g, "\n"); } getValidParentNodeToInheritDocumentation(baseNode) { if (!baseNode.baseFunctions || baseNode.baseFunctions.length !== 1) { return null; } const parentNode = this.deref("FunctionDefinition", baseNode.baseFunctions[0]); if ((0, utils_1.isNodeType)("VariableDeclaration", baseNode)) { return parentNode; } if (baseNode.parameters.parameters.length !== parentNode.parameters.parameters.length) { return null; } for (let i = 0; i < baseNode.parameters.parameters.length; i++) { if (baseNode.parameters.parameters[i].name !== parentNode.parameters.parameters[i].name) { return null; } } return parentNode; } findFunctionDefinitionByContractName(node, contractName) { const contract = this.deref("ContractDefinition", node.scope); if (contract.canonicalName === contractName || contract.name === contractName) { return node; } if (!node.baseFunctions) { return undefined; } for (let i = 0; i < node.baseFunctions.length; i++) { const baseFunction = this.deref("FunctionDefinition", node.baseFunctions[i]); const result = this.findFunctionDefinitionByContractName(baseFunction, contractName); if (result) { return result; } } } findModifierDefinitionByContractName(node, contractName) { // Since we cannot access the scope (Contract) in which the modifier is defined, // we are looking for the topmost parent modifier documentation without the @inheritdoc tag. const inheritDocsRegex = /@inheritdoc (\w+)/gm; const matches = inheritDocsRegex.exec(node.documentation?.text); if (!matches) { return node; } if (!node.baseModifiers) { return undefined; } for (let i = 0; i < node.baseModifiers.length; i++) { const baseModifier = this.deref("ModifierDefinition", node.baseModifiers[i]); const result = this.findModifierDefinitionByContractName(baseModifier, contractName); if (result) { return result; } } } parseNameAndDescription(text) { const nameAndDescriptionRegex = /^(\w+).? ([\s\S]*)?/gm; const matches = nameAndDescriptionRegex.exec(text); if (!matches) { throw new Error(`Invalid name and description: ${text}`); } const [, name, description] = matches; return [name, description]; } joinDescriptionLines(text) { return text.replace(/\n/g, " "); } parseNatSpecDocumentation(baseNode) { const natSpec = {}; const nodes = [baseNode]; for (let i = 0; i < nodes.length; i++) { let node = nodes[i]; if (!node.documentation) { const parentNode = this.getValidParentNodeToInheritDocumentation(node); if (parentNode) { nodes.push(parentNode); } continue; } let sourceText = this.parseStringFromSourceCode(node.documentation.src); if (sourceText) { const text = this.deleteCommentSymbols(sourceText); const natSpecRegex = /^(?:@(\w+|custom:[a-z][a-z-]*) )?((?:(?!^@(?:\w+|custom:[a-z][a-z-]*) )[^])*)/gm; const matches = [...text.matchAll(natSpecRegex)]; for (let i = 0; i < matches.length; i++) { const [, tag = "notice", rawText] = matches[i]; const text = this.replaceMultipleNewLinesWithOne(rawText); switch (tag) { case "title": { break; } case "author": { if (!natSpec.author) { natSpec.author = text; } break; } case "notice": { if (!natSpec.notice) { natSpec.notice = text; } break; } case "dev": { if (!natSpec.dev) { natSpec.dev = []; } natSpec.dev.push(text); break; } case "param": { natSpec.params ?? (natSpec.params = []); const [paramName, paramDescriptionRaw] = this.parseNameAndDescription(text); const paramDescription = this.joinDescriptionLines(paramDescriptionRaw); // if tag is already defined, skip it if (natSpec.params.find((param) => param.name == paramName)) { continue; } let params; if (node.nodeType === "EnumDefinition" || node.nodeType === "StructDefinition") { params = node.members; } else { params = node.parameters.parameters; } const variableDeclaration = params.find((param) => param.name == paramName); // so that tag was not found, and it may be a return tag if (!variableDeclaration) { // protection from infinite loop if (node.nodeType !== "EventDefinition") { matches[i][1] = "return"; i--; } break; } const type = variableDeclaration.typeDescriptions?.typeString || undefined; natSpec.params.push({ name: paramName, type: type, description: paramDescription }); break; } case "return": { // there is no return tag in events, so it is a param tag if (node.nodeType === "EventDefinition") { matches[i][1] = "param"; i--; break; } natSpec.returns ?? (natSpec.returns = []); let currentParameter = (0, utils_1.isNodeType)("FunctionDefinition", node) ? node.returnParameters.parameters[natSpec.returns.length] : node; if (!currentParameter) { break; } const currentParameterName = currentParameter.name; const type = currentParameter.typeDescriptions?.typeString; // if name is not defined for return parameter if (!currentParameterName) { natSpec.returns.push({ type: type, description: this.joinDescriptionLines(text) }); } else { const [paramName, paramDescriptionRaw] = this.parseNameAndDescription(text); const paramDescription = this.joinDescriptionLines(paramDescriptionRaw); if (paramName !== currentParameterName) { break; } natSpec.returns.push({ name: paramName, type: type, description: paramDescription }); } break; } case tag.startsWith("custom:") ? tag : "": { const customTag = tag.replace("custom:", ""); natSpec.custom ?? (natSpec.custom = {}); natSpec.custom[customTag] = text; break; } case "inheritdoc": { const parentNodeRegex = /^(\w+)$/gm; const matches = parentNodeRegex.exec(text); if (!matches) { throw new Error(`Invalid inheritdoc tag: ${text}`); } const [, parentName] = matches; let parentNode; if (node.nodeType === "FunctionDefinition") { parentNode = this.findFunctionDefinitionByContractName(node, parentName); } else if (node.nodeType === "ModifierDefinition") { parentNode = this.findModifierDefinitionByContractName(node, parentName); } else if (node.nodeType === "VariableDeclaration") { break; } if (!parentNode) { throw new Error(`Invalid inheritdoc tag: ${text}`); } nodes.push(parentNode); break; } default: { throw new Error(`Unknown tag: ${tag}`); } } } } } return natSpec; } } exports.Parser = Parser; //# sourceMappingURL=Parser.js.map