Source code for svdoc.fixer

"""svdoc --fix: insert `///< TODO` next to undocumented ports/params, and a
`/** @brief TODO */` stub above an undocumented module, in place. Mirrors
`ruff --fix` — scaffolds missing docs rather than guessing their content.
"""

import pyslang

from .parser import _leading_doc, _trailing_doc


def _end_offset(node) -> int:
    """node may be a raw Token (comma, closing paren) or a syntax node."""
    tok = node.getLastToken() if hasattr(node, "getLastToken") else node
    return tok.range.end.offset


def _brief_stub(text: str, offset: int) -> str:
    """A `/** @brief TODO */` block stub, indented to match the column the
    node it precedes starts at (mid-file insertions -- e.g. above a nested
    modport -- land at the modport's own indentation, not column 0)."""
    line_start = text.rfind("\n", 0, offset) + 1
    indent = text[line_start:offset]
    return f"/**\n{indent} * @brief TODO\n{indent} */\n{indent}"


[docs] def fix_file(path: str) -> bool: """Scaffold missing doc comments in place, in ``path``, and save the result. Inserts ``///< TODO`` next to any undocumented port/parameter, and a ``/** @brief TODO */`` stub above the module/interface/package itself if it has no doc comment. Idempotent: re-running on an already-documented file makes no changes. :param path: Path to the ``.sv`` file to fix in place. :returns: ``True`` if the file was modified, ``False`` if it was already fully documented. :raises ValueError: If the file fails to parse cleanly. """ tree = pyslang.syntax.SyntaxTree.fromFile(path, pyslang.SourceManager()) if tree.diagnostics: raise ValueError(f"parse errors in {path}: {list(tree.diagnostics)}") with open(path) as f: text = f.read() # module and interface declarations share the same syntax shape # (ModuleDeclarationSyntax, differentiated only by .kind) -- --fix scaffolds # their header (params/ports) doc comments identically. Interface bodies # (signals/modports) are scaffolded separately below, since modules don't # have those constructs. mod = next(m for m in tree.root.members if hasattr(m, "header")) header = mod.header # (offset, text_to_insert), applied back-to-front so earlier offsets stay valid. insertions = [] if _leading_doc(mod) is None: offset = mod.getFirstToken().range.start.offset insertions.append((offset, _brief_stub(text, offset))) if header.parameters: raw = list(header.parameters.declarations) for i, node in enumerate(raw): if node.kind != pyslang.syntax.SyntaxKind.ParameterDeclaration: continue has_comma = i + 1 < len(raw) # trailing ///< doc for this item is leading trivia on the node # AFTER its separator (see parser._trailing_doc) -- for the last # item there's no comma, so the lookup and insertion point are # both the item itself, right before the closing paren. lookup = raw[i + 2] if has_comma and i + 2 < len(raw) else header.parameters.closeParen if _trailing_doc(lookup) is None: offset = _end_offset(raw[i + 1]) if has_comma else _end_offset(node) insertions.append((offset, " ///< TODO")) if header.ports: raw = list(header.ports.ports) for i, node in enumerate(raw): if not hasattr(node, "declarator"): continue has_comma = i + 1 < len(raw) lookup = raw[i + 2] if has_comma and i + 2 < len(raw) else header.ports.closeParen if _trailing_doc(lookup) is None: offset = _end_offset(raw[i + 1]) if has_comma else _end_offset(node) insertions.append((offset, " ///< TODO")) if mod.kind == pyslang.syntax.SyntaxKind.InterfaceDeclaration: members = list(mod.members) for i, m in enumerate(members): next_node = members[i + 1] if i + 1 < len(members) else mod.endmodule if m.kind == pyslang.syntax.SyntaxKind.DataDeclaration and _trailing_doc(next_node) is None: insertions.append((_end_offset(m), " ///< TODO")) elif m.kind == pyslang.syntax.SyntaxKind.ModportDeclaration and _leading_doc(m) is None: offset = m.getFirstToken().range.start.offset insertions.append((offset, _brief_stub(text, offset))) if not insertions: return False for offset, snippet in sorted(insertions, reverse=True): text = text[:offset] + snippet + text[offset:] with open(path, "w") as f: f.write(text) return True