github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-bedrock/scripts/autogen/generate-invariant-docs.ts (about) 1 import fs from 'fs' 2 import path from 'path' 3 4 const ROOT_DIR = path.join(__dirname, '..', '..') 5 const BASE_INVARIANTS_DIR = path.join(ROOT_DIR, 'test', 'invariants') 6 const BASE_DOCS_DIR = path.join(ROOT_DIR, 'invariant-docs') 7 const BASE_INVARIANT_GH_URL = '../test/invariants/' 8 const NATSPEC_INV = '@custom:invariant' 9 10 // Represents an invariant test contract 11 type Contract = { 12 name: string 13 fileName: string 14 docs: InvariantDoc[] 15 } 16 17 // Represents the documentation of an invariant 18 type InvariantDoc = { 19 header?: string 20 desc?: string 21 lineNo?: number 22 } 23 24 const writtenFiles = [] 25 26 // Lazy-parses all test files in the `test/invariants` directory 27 // to generate documentation on all invariant tests. 28 const docGen = (dir: string): void => { 29 // Grab all files within the invariants test dir 30 const files = fs.readdirSync(dir) 31 32 // Array to store all found invariant documentation comments. 33 const docs: Contract[] = [] 34 35 for (const fileName of files) { 36 // Read the contents of the invariant test file. 37 const fileContents = fs.readFileSync(path.join(dir, fileName)).toString() 38 39 // Split the file into individual lines and trim whitespace. 40 const lines = fileContents.split('\n').map((line: string) => line.trim()) 41 42 // Create an object to store all invariant test docs for the current contract 43 const name = fileName.replace('.t.sol', '') 44 const contract: Contract = { name, fileName, docs: [] } 45 46 let currentDoc: InvariantDoc 47 48 // Loop through all lines to find comments. 49 for (let i = 0; i < lines.length; i++) { 50 let line = lines[i] 51 52 // We have an invariant doc 53 if (line.startsWith(`/// ${NATSPEC_INV}`)) { 54 // Assign the header of the invariant doc. 55 // TODO: Handle ambiguous case for `INVARIANT: ` prefix. 56 currentDoc = { 57 header: line.replace(`/// ${NATSPEC_INV}`, '').trim(), 58 desc: '', 59 } 60 61 // If the header is multi-line, continue appending to the `currentDoc`'s header. 62 line = lines[++i] 63 while (line.startsWith(`///`) && line.trim() !== '///') { 64 currentDoc.header += ` ${line.replace(`///`, '').trim()}` 65 line = lines[++i] 66 } 67 68 // Process the description 69 while ((line = lines[++i]).startsWith('///')) { 70 line = line.replace('///', '').trim() 71 72 // If the line has any contents, insert it into the desc. 73 // Otherwise, consider it a linebreak. 74 currentDoc.desc += line.length > 0 ? `${line} ` : '\n' 75 } 76 77 // Set the line number of the test 78 currentDoc.lineNo = i + 1 79 80 // Add the doc to the contract 81 contract.docs.push(currentDoc) 82 } 83 } 84 85 // Add the contract to the array of docs 86 docs.push(contract) 87 } 88 89 for (const contract of docs) { 90 const fileName = path.join(BASE_DOCS_DIR, `${contract.name}.md`) 91 const alreadyWritten = writtenFiles.includes(fileName) 92 93 // If the file has already been written, append the extra docs to the end. 94 // Otherwise, write the file from scratch. 95 fs.writeFileSync( 96 fileName, 97 alreadyWritten 98 ? `${fs.readFileSync(fileName)}\n${renderContractDoc(contract, false)}` 99 : renderContractDoc(contract, true) 100 ) 101 102 // If the file was just written for the first time, add it to the list of written files. 103 if (!alreadyWritten) { 104 writtenFiles.push(fileName) 105 } 106 } 107 108 console.log( 109 `Generated invariant test documentation for:\n - ${ 110 docs.length 111 } contracts\n - ${docs.reduce( 112 (acc: number, contract: Contract) => acc + contract.docs.length, 113 0 114 )} invariant tests\nsuccessfully!` 115 ) 116 } 117 118 // Generate a table of contents for all invariant docs and place it in the README. 119 const tocGen = (): void => { 120 const autoTOCPrefix = '<!-- START autoTOC -->\n' 121 const autoTOCPostfix = '<!-- END autoTOC -->\n' 122 123 // Grab the name of all markdown files in `BASE_DOCS_DIR` except for `README.md`. 124 const files = fs 125 .readdirSync(BASE_DOCS_DIR) 126 .filter((fileName: string) => fileName !== 'README.md') 127 128 // Generate a table of contents section. 129 const tocList = files 130 .map( 131 (fileName: string) => `- [${fileName.replace('.md', '')}](./${fileName})` 132 ) 133 .join('\n') 134 const toc = `${autoTOCPrefix}\n## Table of Contents\n${tocList}\n${autoTOCPostfix}` 135 136 // Write the table of contents to the README. 137 const readmeContents = fs 138 .readFileSync(path.join(BASE_DOCS_DIR, 'README.md')) 139 .toString() 140 const above = readmeContents.split(autoTOCPrefix)[0] 141 const below = readmeContents.split(autoTOCPostfix)[1] 142 fs.writeFileSync( 143 path.join(BASE_DOCS_DIR, 'README.md'), 144 `${above}${toc}${below}` 145 ) 146 } 147 148 // Render a `Contract` object into valid markdown. 149 const renderContractDoc = (contract: Contract, header: boolean): string => { 150 const _header = header ? `# \`${contract.name}\` Invariants\n` : '' 151 const docs = contract.docs 152 .map((doc: InvariantDoc) => { 153 const line = `${contract.fileName}#L${doc.lineNo}` 154 return `## ${doc.header}\n**Test:** [\`${line}\`](${BASE_INVARIANT_GH_URL}${line})\n\n${doc.desc}` 155 }) 156 .join('\n\n') 157 return `${_header}\n${docs}` 158 } 159 160 // Generate the docs 161 162 // Forge 163 console.log('Generating docs for forge invariants...') 164 docGen(BASE_INVARIANTS_DIR) 165 166 // New line 167 console.log() 168 169 // Generate an updated table of contents 170 tocGen()