138
scripts/check_no_docstring_types.py
Normal file
138
scripts/check_no_docstring_types.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that docstrings do not contain type annotations.
|
||||
|
||||
This script enforces that Google-style docstrings contain NO type information,
|
||||
since types should be declared in function signatures using type hints.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def extract_docstring(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str | None:
|
||||
"""Extract docstring from a function node."""
|
||||
if (
|
||||
node.body
|
||||
and isinstance(node.body[0], ast.Expr)
|
||||
and isinstance(node.body[0].value, ast.Constant)
|
||||
and isinstance(node.body[0].value.value, str)
|
||||
):
|
||||
return node.body[0].value.value
|
||||
return None
|
||||
|
||||
|
||||
def check_docstring_for_types(docstring: str, func_name: str, filename: str, lineno: int) -> list[str]:
|
||||
"""Check if docstring contains type annotations and return errors."""
|
||||
errors: list[str] = []
|
||||
|
||||
lines = docstring.split('\n')
|
||||
in_args_section = False
|
||||
in_returns_section = False
|
||||
in_yields_section = False
|
||||
|
||||
for i, line in enumerate(lines, 1):
|
||||
line_stripped = line.strip()
|
||||
|
||||
# Track which section we're in
|
||||
if re.match(r'^\s*Args?\s*:', line_stripped, re.IGNORECASE):
|
||||
in_args_section = True
|
||||
in_returns_section = False
|
||||
in_yields_section = False
|
||||
continue
|
||||
elif re.match(r'^\s*Returns?\s*:', line_stripped, re.IGNORECASE):
|
||||
in_args_section = False
|
||||
in_returns_section = True
|
||||
in_yields_section = False
|
||||
continue
|
||||
elif re.match(r'^\s*Yields?\s*:', line_stripped, re.IGNORECASE):
|
||||
in_args_section = False
|
||||
in_returns_section = False
|
||||
in_yields_section = True
|
||||
continue
|
||||
elif re.match(r'^\s*(Raises?|Examples?|Note|Notes)\s*:', line_stripped, re.IGNORECASE):
|
||||
in_args_section = False
|
||||
in_returns_section = False
|
||||
in_yields_section = False
|
||||
continue
|
||||
|
||||
# Check for type annotations in Args section
|
||||
if in_args_section and re.match(r'^\s*\w+\s*\([^)]+\)\s*:', line_stripped):
|
||||
errors.append(
|
||||
f"{filename}:{lineno + i}:{func_name}: "
|
||||
f"Type annotation found in Args section: '{line_stripped}'. "
|
||||
f"Remove type information and use function signature type hints instead."
|
||||
)
|
||||
|
||||
# Check for type annotations in Returns section
|
||||
if in_returns_section and re.match(r'^\s*\w+\s*:', line_stripped) and not re.match(r'^\s*Returns?\s*:', line_stripped, re.IGNORECASE):
|
||||
errors.append(
|
||||
f"{filename}:{lineno + i}:{func_name}: "
|
||||
f"Type annotation found in Returns section: '{line_stripped}'. "
|
||||
f"Remove type information and use function signature return type hints instead."
|
||||
)
|
||||
|
||||
# Check for type annotations in Yields section
|
||||
if in_yields_section and re.match(r'^\s*\w+\s*:', line_stripped) and not re.match(r'^\s*Yields?\s*:', line_stripped, re.IGNORECASE):
|
||||
errors.append(
|
||||
f"{filename}:{lineno + i}:{func_name}: "
|
||||
f"Type annotation found in Yields section: '{line_stripped}'. "
|
||||
f"Remove type information and use function signature type hints instead."
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def check_file(filepath: Path) -> list[str]:
|
||||
"""Check a single Python file for type annotations in docstrings."""
|
||||
try:
|
||||
content = filepath.read_text(encoding='utf-8')
|
||||
tree = ast.parse(content, filename=str(filepath))
|
||||
except (SyntaxError, UnicodeDecodeError) as e:
|
||||
return [f"{filepath}: Failed to parse file: {e}"]
|
||||
|
||||
errors: list[str] = []
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
docstring = extract_docstring(node)
|
||||
if docstring:
|
||||
func_errors = check_docstring_for_types(
|
||||
docstring,
|
||||
node.name,
|
||||
str(filepath),
|
||||
node.lineno
|
||||
)
|
||||
errors.extend(func_errors)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Main function to check all provided files."""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: check_no_docstring_types.py <file1> [file2] ...")
|
||||
return 1
|
||||
|
||||
all_errors: list[str] = []
|
||||
|
||||
for filepath_str in sys.argv[1:]:
|
||||
filepath = Path(filepath_str)
|
||||
if filepath.suffix == '.py':
|
||||
errors = check_file(filepath)
|
||||
all_errors.extend(errors)
|
||||
|
||||
if all_errors:
|
||||
print("❌ Found type annotations in docstrings:")
|
||||
for error in all_errors:
|
||||
print(f" {error}")
|
||||
print("\n💡 Tip: Use type hints in function signatures instead of docstring type annotations.")
|
||||
return 1
|
||||
|
||||
print("✅ No type annotations found in docstrings.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user