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
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
Problem Statement
docvet validates that code behavior is reflected in docstrings for four constructs — but not the most fundamental one:
raise Xmissing-raisesyield/yield frommissing-yields.send()targetmissing-receiveswarnings.warn(...)missing-warnsreturn <expr>Returns:is arguably the most important section afterArgs:— 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) forast.Returnnodes wherenode.value is not None. Barereturnandreturn Noneare excluded (control flow, not meaningful returns).Edge cases
return/return Noneonly — no findingreturn valueand barereturn— finding (at least one meaningful return)__init__— skip (always returns None)__bool__,__len__,__repr__, etc.) — skip (obvious return semantics, consistent withpresence.ignore_magic)def/classExample
Configuration
Implementation sketch
Acceptance Criteria
return <expr>but noReturns:section produce a findingreturnandreturn Noneare excluded__init__, and dunder methods are skippedrequire-returnsadded with defaultTrue_RULE_DISPATCHTechnical Notes
Files changed: config.py (~3 lines), enrichment.py (~40 lines + dispatch entry), tests (~50 lines)
Category:
required— matching precedent frommissing-raisesSphinx/RST interaction: When
docstring-style = "sphinx"(see issue for Sphinx support), section detection recognizes:returns:/:rtype:as equivalent toReturns:.BMAD Workflow
When ready to implement:
/bmad-bmm-quick-spec->/bmad-bmm-quick-dev