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()