Skip to content

Feature: missing-returns enrichment rule #321

@Alberto-Codes

Description

@Alberto-Codes

Problem Statement

docvet validates that code behavior is reflected in docstrings for four constructs — but not the most fundamental one:

Construct AST pattern Rule
Exceptions raise X missing-raises
Generators yield / yield from missing-yields
Coroutine send .send() target missing-receives
Warnings warnings.warn(...) missing-warns
Return values return <expr> not checked

Returns: is arguably the most important section after Args: — it tells the caller what they get back. Every other docstring tool checks this (darglint DAR201, pydoclint DOC201, numpydoc GL08).

Current Behavior

Functions that return values but have no Returns: section produce no finding. docvet checks raises, yields, warns, and receives — but not returns.

Proposed Solution

Detection logic

Walk the function's AST body (scope-aware, same as _check_missing_raises) for ast.Return nodes where node.value is not None. Bare return and return None are excluded (control flow, not meaningful returns).

Edge cases

  • Bare return / return None only — no finding
  • Mix of return value and bare return — finding (at least one meaningful return)
  • Property methods — skip (documented as attributes)
  • __init__ — skip (always returns None)
  • Dunder methods (__bool__, __len__, __repr__, etc.) — skip (obvious return semantics, consistent with presence.ignore_magic)
  • Nested functions — scope-aware walk stops at nested def/class

Example

def get_user(user_id: int) -> User:
    """Fetch a user by their ID.

    Args:
        user_id: The user's database ID.
    """
    # missing-returns: Function 'get_user' returns a value but has no Returns: section
    return db.query(User).get(user_id)

Configuration

[tool.docvet.enrichment]
require-returns = true  # default

Implementation sketch

_SKIP_RETURN_CHECK = frozenset({"__init__", "__del__", "__post_init__"})

def _check_missing_returns(symbol, sections, node_index, config, file_path) -> Finding | None:
    if symbol.kind not in ("function", "method"):
        return None
    if symbol.name in _SKIP_RETURN_CHECK:
        return None
    if symbol.name.startswith("__") and symbol.name.endswith("__"):
        return None
    node = node_index.get(symbol.line)
    if node is None:
        return None
    if "Returns" in sections:
        return None
    # Check for @property — skip
    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
        for decorator in node.decorator_list:
            if isinstance(decorator, ast.Name) and decorator.id == "property":
                return None
    # Scope-aware walk for return statements with values
    has_return_value = False
    stack = list(ast.iter_child_nodes(node))
    while stack:
        child = stack.pop()
        if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            continue
        if isinstance(child, ast.Return) and child.value is not None:
            has_return_value = True
            break
        stack.extend(ast.iter_child_nodes(child))
    if not has_return_value:
        return None
    return Finding(
        file=file_path, line=symbol.line, symbol=symbol.name,
        rule="missing-returns",
        message=f"Function '{symbol.name}' returns a value but has no Returns: section",
        category="required",
    )

Acceptance Criteria

  • Functions with return <expr> but no Returns: section produce a finding
  • Bare return and return None are excluded
  • Properties, __init__, and dunder methods are skipped
  • Scope-aware (nested functions don't leak)
  • Config key require-returns added with default True
  • Wired into _RULE_DISPATCH

Technical Notes

Files changed: config.py (~3 lines), enrichment.py (~40 lines + dispatch entry), tests (~50 lines)

Category: required — matching precedent from missing-raises

Sphinx/RST interaction: When docstring-style = "sphinx" (see issue for Sphinx support), section detection recognizes :returns: / :rtype: as equivalent to Returns:.


BMAD Workflow

When ready to implement:

  • /bmad-bmm-quick-spec -> /bmad-bmm-quick-dev

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions