Source code for rude.rules.pyflakes.docstrings
"""
Docstring rules: doctest validation.
F721: DoctestSyntaxError - syntax error in doctest
"""
from __future__ import annotations
import ast
import doctest
from collections.abc import Iterator
from typing import TYPE_CHECKING, ClassVar
from rude.core.node_types import NodeType
from rude.core.rule import Rule
from rude.core.types import Diagnostic, Severity
from rude.utils import extract_string_content
if TYPE_CHECKING:
from rude.core.node import Node
[docs]
class DoctestSyntaxError(Rule):
"""
F721: Syntax error in doctest.
Rationale: Doctest examples with invalid syntax will fail when
run, and may indicate stale or incorrect documentation.
Example::
# Bad
def foo():
'''
>>> x = [1, 2, 3
''' # F721 - unclosed bracket in doctest
pass
# Good
def foo():
'''
>>> x = [1, 2, 3]
'''
pass
"""
code: ClassVar[str] = "F721"
message: ClassVar[str] = "syntax error in doctest"
severity: ClassVar[Severity] = Severity.ERROR
node_types = {NodeType.STRING}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
# Check if this string is a docstring
if not self._is_docstring(node):
return
# Extract string content
content = extract_string_content(node.text)
if content is None:
return
# Check for doctest examples
# Skip if no doctest markers
if ">>>" not in content:
return
# Parse doctest examples
parser = doctest.DocTestParser()
try:
examples = parser.get_examples(content)
except ValueError:
# Invalid doctest format
return
# Check each example for syntax errors
for example in examples:
try:
# Try to parse the source as Python
ast.parse(example.source)
except SyntaxError as e:
# Calculate the line number
# example.lineno is 0-indexed from start of docstring content
# e.lineno is 1-indexed within the example
example_line = example.lineno + 1 # 1-indexed
error_offset = (e.lineno or 1) - 1
actual_line = node.line + example_line + error_offset
yield self.diagnostic_at(actual_line, 0, self.message)
def _is_docstring(self, node: Node) -> bool:
"""Check if this string is a docstring."""
# Quick-reject: docstrings are always in expression_statement
if node.parent_type != "expression_statement":
return False
parent = node.parent
if parent is None:
return False
gp = parent.parent
if not gp:
return False
# Module-level docstring
if gp.type == "module":
# Check if this is the first statement
for child in gp.children:
if child.type == "expression_statement":
return child.raw.id == parent.raw.id
# Skip comments
if child.type == "comment":
continue
# Any other statement means this is not the first
return False
return False
# Function/class docstring
if gp.type in ("function_definition", "class_definition"):
# The docstring should be in the body block
body = gp.child_by_field("body")
if body and body.type == "block":
# Check if this is the first statement in the block
for child in body.children:
if child.type == "expression_statement":
return child.raw.id == parent.raw.id
# Skip comments, newlines
if child.type in ("comment", "\n"):
continue
if not child.raw.is_named:
continue
# Any other named node means this is not the first
return False
# Also check if parent of expression_statement is a block
if gp.type == "block":
# Check if this is the first statement
ggp = gp.parent
if ggp and ggp.type in ("function_definition", "class_definition"):
for child in gp.children:
if child.type == "expression_statement":
return child.raw.id == parent.raw.id
if child.type in ("comment", "\n"):
continue
if not child.raw.is_named:
continue
return False
return False
DOCSTRING_RULES = [
DoctestSyntaxError,
]
__all__ = [
"DOCSTRING_RULES",
"DoctestSyntaxError",
]