pycodecommenter 2.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyCodeCommenter/__init__.py +26 -0
- PyCodeCommenter/cli.py +72 -0
- PyCodeCommenter/commenter.py +450 -0
- PyCodeCommenter/coverage.py +124 -0
- PyCodeCommenter/docstring_parser.py +121 -0
- PyCodeCommenter/parameter_descriptions.py +76 -0
- PyCodeCommenter/templates.py +95 -0
- PyCodeCommenter/type_analyzer.py +203 -0
- PyCodeCommenter/validator.py +563 -0
- pycodecommenter-2.0.3.dist-info/METADATA +281 -0
- pycodecommenter-2.0.3.dist-info/RECORD +15 -0
- pycodecommenter-2.0.3.dist-info/WHEEL +5 -0
- pycodecommenter-2.0.3.dist-info/entry_points.txt +2 -0
- pycodecommenter-2.0.3.dist-info/licenses/LICENSE +21 -0
- pycodecommenter-2.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docstring validation module for PyCodeCommenter.
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive validation of Python docstrings against actual
|
|
5
|
+
code signatures. It checks for signature mismatches, type consistency, exception
|
|
6
|
+
documentation, return documentation, format compliance, and content quality.
|
|
7
|
+
|
|
8
|
+
Classes:
|
|
9
|
+
Severity: Enum for issue severity levels (ERROR, WARNING, INFO)
|
|
10
|
+
ValidationIssue: Represents a single documentation issue
|
|
11
|
+
ValidationStats: Statistics from validation run
|
|
12
|
+
ValidationReport: Container for validation results with reporting capabilities
|
|
13
|
+
DocstringValidator: Main validator class for checking documentation quality
|
|
14
|
+
"""
|
|
15
|
+
import ast
|
|
16
|
+
import logging
|
|
17
|
+
from typing import List, Dict, Optional, Set, Any, Union
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import Enum
|
|
20
|
+
try:
|
|
21
|
+
from .docstring_parser import DocstringParser
|
|
22
|
+
except (ImportError, ValueError):
|
|
23
|
+
from docstring_parser import DocstringParser
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
class Severity(Enum):
|
|
28
|
+
ERROR = "error" # Must fix (missing required docs)
|
|
29
|
+
WARNING = "warning" # Should fix (inconsistencies)
|
|
30
|
+
INFO = "info" # Nice to have (style issues)
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ValidationIssue:
|
|
34
|
+
"""Represents a single documentation issue."""
|
|
35
|
+
severity: Severity
|
|
36
|
+
category: str # "signature", "types", "exceptions", "format", "quality"
|
|
37
|
+
location: str # "file.py:line:function_name"
|
|
38
|
+
message: str
|
|
39
|
+
suggestion: Optional[str] = None # How to fix it
|
|
40
|
+
|
|
41
|
+
def __str__(self):
|
|
42
|
+
return f"[{self.severity.value.upper()}] {self.location}: {self.message}"
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ValidationStats:
|
|
46
|
+
"""Statistics from validation run."""
|
|
47
|
+
total_functions: int = 0
|
|
48
|
+
total_classes: int = 0
|
|
49
|
+
documented_functions: int = 0
|
|
50
|
+
documented_classes: int = 0
|
|
51
|
+
total_issues: int = 0
|
|
52
|
+
errors: int = 0
|
|
53
|
+
warnings: int = 0
|
|
54
|
+
infos: int = 0
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def coverage_percentage(self) -> float:
|
|
58
|
+
total = self.total_functions + self.total_classes
|
|
59
|
+
documented = self.documented_functions + self.documented_classes
|
|
60
|
+
return (documented / total * 100) if total > 0 else 0.0
|
|
61
|
+
|
|
62
|
+
class ValidationReport:
|
|
63
|
+
"""Container for validation results with reporting capabilities."""
|
|
64
|
+
|
|
65
|
+
def __init__(self):
|
|
66
|
+
self.issues: List[ValidationIssue] = []
|
|
67
|
+
self.stats = ValidationStats()
|
|
68
|
+
self.file_path: Optional[str] = None
|
|
69
|
+
|
|
70
|
+
def add_issue(self, issue: ValidationIssue):
|
|
71
|
+
"""Add an issue and update stats."""
|
|
72
|
+
self.issues.append(issue)
|
|
73
|
+
self.stats.total_issues += 1
|
|
74
|
+
if issue.severity == Severity.ERROR:
|
|
75
|
+
self.stats.errors += 1
|
|
76
|
+
elif issue.severity == Severity.WARNING:
|
|
77
|
+
self.stats.warnings += 1
|
|
78
|
+
else:
|
|
79
|
+
self.stats.infos += 1
|
|
80
|
+
|
|
81
|
+
def print_summary(self):
|
|
82
|
+
"""Print human-readable summary to console."""
|
|
83
|
+
print("\n" + "="*60)
|
|
84
|
+
print("VALIDATION REPORT")
|
|
85
|
+
print("="*60)
|
|
86
|
+
print(f"File: {self.file_path or 'N/A'}")
|
|
87
|
+
print(f"Coverage: {self.stats.coverage_percentage:.1f}%")
|
|
88
|
+
print(f"Total Issues: {self.stats.total_issues}")
|
|
89
|
+
print(f" - Errors: {self.stats.errors}")
|
|
90
|
+
print(f" - Warnings: {self.stats.warnings}")
|
|
91
|
+
print(f" - Info: {self.stats.infos}")
|
|
92
|
+
|
|
93
|
+
if self.issues:
|
|
94
|
+
print("\nISSUES:")
|
|
95
|
+
for issue in self.issues:
|
|
96
|
+
print(f"\n{issue}")
|
|
97
|
+
if issue.suggestion:
|
|
98
|
+
print(f" → Suggestion: {issue.suggestion}")
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict:
|
|
101
|
+
"""Convert to dictionary for JSON export."""
|
|
102
|
+
return {
|
|
103
|
+
"file": self.file_path,
|
|
104
|
+
"stats": {
|
|
105
|
+
"coverage": self.stats.coverage_percentage,
|
|
106
|
+
"total_issues": self.stats.total_issues,
|
|
107
|
+
"errors": self.stats.errors,
|
|
108
|
+
"warnings": self.stats.warnings,
|
|
109
|
+
"infos": self.stats.infos
|
|
110
|
+
},
|
|
111
|
+
"issues": [
|
|
112
|
+
{
|
|
113
|
+
"severity": issue.severity.value,
|
|
114
|
+
"category": issue.category,
|
|
115
|
+
"location": issue.location,
|
|
116
|
+
"message": issue.message,
|
|
117
|
+
"suggestion": issue.suggestion
|
|
118
|
+
}
|
|
119
|
+
for issue in self.issues
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
def to_markdown(self) -> str:
|
|
124
|
+
"""Generate markdown report."""
|
|
125
|
+
md = f"# Validation Report\n\n"
|
|
126
|
+
md += f"**File:** {self.file_path or 'N/A'}\n\n"
|
|
127
|
+
md += f"## Summary\n\n"
|
|
128
|
+
md += f"- **Coverage:** {self.stats.coverage_percentage:.1f}%\n"
|
|
129
|
+
md += f"- **Total Issues:** {self.stats.total_issues}\n"
|
|
130
|
+
md += f" - Errors: {self.stats.errors}\n"
|
|
131
|
+
md += f" - Warnings: {self.stats.warnings}\n"
|
|
132
|
+
md += f" - Info: {self.stats.infos}\n\n"
|
|
133
|
+
|
|
134
|
+
if self.issues:
|
|
135
|
+
md += f"## Issues\n\n"
|
|
136
|
+
for issue in self.issues:
|
|
137
|
+
md += f"### {issue.severity.value.upper()}: {issue.message}\n\n"
|
|
138
|
+
md += f"- **Location:** `{issue.location}`\n"
|
|
139
|
+
md += f"- **Category:** {issue.category}\n"
|
|
140
|
+
if issue.suggestion:
|
|
141
|
+
md += f"- **Suggestion:** {issue.suggestion}\n"
|
|
142
|
+
md += "\n"
|
|
143
|
+
|
|
144
|
+
return md
|
|
145
|
+
|
|
146
|
+
class DocstringValidator:
|
|
147
|
+
"""Main validator class for checking documentation quality."""
|
|
148
|
+
|
|
149
|
+
def __init__(self, code_string: Optional[str] = None, file_path: Optional[str] = None):
|
|
150
|
+
"""
|
|
151
|
+
Initializes the validator.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
code_string (Optional[str]): The code to validate as a string.
|
|
155
|
+
file_path (Optional[str]): The path to the file to validate.
|
|
156
|
+
"""
|
|
157
|
+
self.code = code_string
|
|
158
|
+
self.file_path = file_path
|
|
159
|
+
self.parsed_code = None
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
if code_string:
|
|
163
|
+
self.parsed_code = ast.parse(code_string)
|
|
164
|
+
elif file_path:
|
|
165
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
166
|
+
self.code = f.read()
|
|
167
|
+
self.parsed_code = ast.parse(self.code)
|
|
168
|
+
except (FileNotFoundError, IOError) as e:
|
|
169
|
+
logger.error(f"Error reading file {file_path}: {e}")
|
|
170
|
+
except SyntaxError as e:
|
|
171
|
+
logger.error(f"Syntax error in code: {e}")
|
|
172
|
+
|
|
173
|
+
def validate_all(self) -> ValidationReport:
|
|
174
|
+
"""Run all validation checks and return comprehensive report."""
|
|
175
|
+
report = ValidationReport()
|
|
176
|
+
report.file_path = self.file_path
|
|
177
|
+
|
|
178
|
+
if not self.parsed_code:
|
|
179
|
+
return report
|
|
180
|
+
|
|
181
|
+
# Traverse AST and validate each function/class
|
|
182
|
+
for node in ast.walk(self.parsed_code):
|
|
183
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
184
|
+
self._validate_function(node, report)
|
|
185
|
+
report.stats.total_functions += 1
|
|
186
|
+
elif isinstance(node, ast.ClassDef):
|
|
187
|
+
self._validate_class(node, report)
|
|
188
|
+
report.stats.total_classes += 1
|
|
189
|
+
|
|
190
|
+
return report
|
|
191
|
+
|
|
192
|
+
def _validate_function(self, func_node: Union[ast.FunctionDef, ast.AsyncFunctionDef], report: ValidationReport) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Validate a single function.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
func_node (Union[ast.FunctionDef, ast.AsyncFunctionDef]): The function node to validate.
|
|
198
|
+
report (ValidationReport): The report to add issues to.
|
|
199
|
+
"""
|
|
200
|
+
docstring = ast.get_docstring(func_node)
|
|
201
|
+
location = f"{self.file_path or 'code'}:{func_node.lineno}:{func_node.name}"
|
|
202
|
+
|
|
203
|
+
# Check if documented
|
|
204
|
+
if not docstring:
|
|
205
|
+
report.add_issue(ValidationIssue(
|
|
206
|
+
severity=Severity.ERROR,
|
|
207
|
+
category="missing",
|
|
208
|
+
location=location,
|
|
209
|
+
message=f"Function '{func_node.name}' has no docstring",
|
|
210
|
+
suggestion="Add a docstring with at minimum a summary line"
|
|
211
|
+
))
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
report.stats.documented_functions += 1
|
|
215
|
+
|
|
216
|
+
# Run all checks
|
|
217
|
+
issues = []
|
|
218
|
+
issues.extend(self.check_signature_match(func_node, docstring, location))
|
|
219
|
+
issues.extend(self.check_type_consistency(func_node, docstring, location))
|
|
220
|
+
issues.extend(self.check_exception_documentation(func_node, docstring, location))
|
|
221
|
+
issues.extend(self.check_return_documentation(func_node, docstring, location))
|
|
222
|
+
issues.extend(self.check_format_compliance(docstring, location))
|
|
223
|
+
issues.extend(self.check_content_quality(docstring, location))
|
|
224
|
+
|
|
225
|
+
for issue in issues:
|
|
226
|
+
report.add_issue(issue)
|
|
227
|
+
|
|
228
|
+
def _validate_class(self, class_node: ast.ClassDef, report: ValidationReport) -> None:
|
|
229
|
+
"""
|
|
230
|
+
Validate a single class.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
class_node (ast.ClassDef): The class node to validate.
|
|
234
|
+
report (ValidationReport): The report to add issues to.
|
|
235
|
+
"""
|
|
236
|
+
docstring = ast.get_docstring(class_node)
|
|
237
|
+
location = f"{self.file_path or 'code'}:{class_node.lineno}:{class_node.name}"
|
|
238
|
+
|
|
239
|
+
if not docstring:
|
|
240
|
+
report.add_issue(ValidationIssue(
|
|
241
|
+
severity=Severity.ERROR,
|
|
242
|
+
category="missing",
|
|
243
|
+
location=location,
|
|
244
|
+
message=f"Class '{class_node.name}' has no docstring",
|
|
245
|
+
suggestion="Add a class docstring describing its purpose"
|
|
246
|
+
))
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
report.stats.documented_classes += 1
|
|
250
|
+
|
|
251
|
+
def check_signature_match(self, func_node: Union[ast.FunctionDef, ast.AsyncFunctionDef], docstring: str, location: str) -> List[ValidationIssue]:
|
|
252
|
+
"""
|
|
253
|
+
Verify all function parameters are documented and vice versa.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
func_node (Union[ast.FunctionDef, ast.AsyncFunctionDef]): The function node.
|
|
257
|
+
docstring (str): The docstring to check.
|
|
258
|
+
location (str): The location string for issues.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List[ValidationIssue]: List of issues found.
|
|
262
|
+
"""
|
|
263
|
+
issues = []
|
|
264
|
+
parser = DocstringParser(docstring)
|
|
265
|
+
doc_params = list(parser.params.keys())
|
|
266
|
+
|
|
267
|
+
# Actual arguments from AST
|
|
268
|
+
actual_params = [arg.arg for arg in func_node.args.args]
|
|
269
|
+
|
|
270
|
+
# Handle self/cls for methods
|
|
271
|
+
is_method = False
|
|
272
|
+
# Simple heuristic: if it's inside a ClassDef (but we don't have parent info easily here without extra logic)
|
|
273
|
+
# Better: check if first arg is self/cls and it's likely a method
|
|
274
|
+
if actual_params and actual_params[0] in ('self', 'cls'):
|
|
275
|
+
actual_params = actual_params[1:]
|
|
276
|
+
is_method = True
|
|
277
|
+
|
|
278
|
+
actual_set = set(actual_params)
|
|
279
|
+
doc_set = set(doc_params)
|
|
280
|
+
|
|
281
|
+
# Missing parameters in docstring
|
|
282
|
+
missing = actual_set - doc_set
|
|
283
|
+
for p in missing:
|
|
284
|
+
issues.append(ValidationIssue(
|
|
285
|
+
severity=Severity.ERROR,
|
|
286
|
+
category="signature",
|
|
287
|
+
location=location,
|
|
288
|
+
message=f"Parameter '{p}' is not documented in docstring",
|
|
289
|
+
suggestion=f"Add '{p}' to the Args section"
|
|
290
|
+
))
|
|
291
|
+
|
|
292
|
+
# Extra parameters in docstring
|
|
293
|
+
extra = doc_set - actual_set
|
|
294
|
+
for p in extra:
|
|
295
|
+
issues.append(ValidationIssue(
|
|
296
|
+
severity=Severity.WARNING,
|
|
297
|
+
category="signature",
|
|
298
|
+
location=location,
|
|
299
|
+
message=f"Parameter '{p}' is documented but not in function signature",
|
|
300
|
+
suggestion=f"Remove '{p}' from docstring or update function signature"
|
|
301
|
+
))
|
|
302
|
+
|
|
303
|
+
# Order mismatch
|
|
304
|
+
# Only check if the sets are the same and non-empty
|
|
305
|
+
if not missing and not extra and actual_params and doc_params:
|
|
306
|
+
if actual_params != doc_params:
|
|
307
|
+
issues.append(ValidationIssue(
|
|
308
|
+
severity=Severity.INFO,
|
|
309
|
+
category="signature",
|
|
310
|
+
location=location,
|
|
311
|
+
message="Parameter order mismatch between signature and docstring",
|
|
312
|
+
suggestion=f"Reorder docstring params to match: {', '.join(actual_params)}"
|
|
313
|
+
))
|
|
314
|
+
|
|
315
|
+
# Self/cls incorrectly documented
|
|
316
|
+
for p in doc_params:
|
|
317
|
+
if p in ('self', 'cls'):
|
|
318
|
+
issues.append(ValidationIssue(
|
|
319
|
+
severity=Severity.WARNING,
|
|
320
|
+
category="signature",
|
|
321
|
+
location=location,
|
|
322
|
+
message=f"'{p}' should not be documented in docstring",
|
|
323
|
+
suggestion=f"Remove '{p}' from Args section"
|
|
324
|
+
))
|
|
325
|
+
|
|
326
|
+
return issues
|
|
327
|
+
|
|
328
|
+
def check_type_consistency(self, func_node: Union[ast.FunctionDef, ast.AsyncFunctionDef], docstring: str, location: str) -> List[ValidationIssue]:
|
|
329
|
+
"""
|
|
330
|
+
Verify type hints match docstring type documentation.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
func_node (Union[ast.FunctionDef, ast.AsyncFunctionDef]): The function node.
|
|
334
|
+
docstring (str): The docstring to check.
|
|
335
|
+
location (str): The location string for issues.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
List[ValidationIssue]: List of issues found.
|
|
339
|
+
"""
|
|
340
|
+
issues = []
|
|
341
|
+
parser = DocstringParser(docstring)
|
|
342
|
+
|
|
343
|
+
# Check return type consistency
|
|
344
|
+
if func_node.returns:
|
|
345
|
+
return_type_hint = ast.unparse(func_node.returns) if hasattr(ast, 'unparse') else str(func_node.returns)
|
|
346
|
+
if not parser.returns:
|
|
347
|
+
issues.append(ValidationIssue(
|
|
348
|
+
severity=Severity.WARNING,
|
|
349
|
+
category="types",
|
|
350
|
+
location=location,
|
|
351
|
+
message=f"Function has return type hint '{return_type_hint}' but no Returns section in docstring",
|
|
352
|
+
suggestion="Add a Returns section documenting the return value"
|
|
353
|
+
))
|
|
354
|
+
|
|
355
|
+
# Check parameter type hints vs docstring
|
|
356
|
+
for arg in func_node.args.args:
|
|
357
|
+
if arg.arg in ('self', 'cls'):
|
|
358
|
+
continue
|
|
359
|
+
if arg.annotation and arg.arg not in parser.params:
|
|
360
|
+
type_hint = ast.unparse(arg.annotation) if hasattr(ast, 'unparse') else str(arg.annotation)
|
|
361
|
+
issues.append(ValidationIssue(
|
|
362
|
+
severity=Severity.INFO,
|
|
363
|
+
category="types",
|
|
364
|
+
location=location,
|
|
365
|
+
message=f"Parameter '{arg.arg}' has type hint '{type_hint}' but is not documented",
|
|
366
|
+
suggestion=f"Document '{arg.arg}' in the Args section"
|
|
367
|
+
))
|
|
368
|
+
|
|
369
|
+
return issues
|
|
370
|
+
|
|
371
|
+
def check_exception_documentation(self, func_node: Union[ast.FunctionDef, ast.AsyncFunctionDef], docstring: str, location: str) -> List[ValidationIssue]:
|
|
372
|
+
"""
|
|
373
|
+
Verify all raised exceptions are documented.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
func_node (Union[ast.FunctionDef, ast.AsyncFunctionDef]): The function node.
|
|
377
|
+
docstring (str): The docstring to check.
|
|
378
|
+
location (str): The location string for issues.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
List[ValidationIssue]: List of issues found.
|
|
382
|
+
"""
|
|
383
|
+
issues = []
|
|
384
|
+
parser = DocstringParser(docstring)
|
|
385
|
+
|
|
386
|
+
# Find all raise statements in function body
|
|
387
|
+
raised_exceptions = set()
|
|
388
|
+
for node in ast.walk(func_node):
|
|
389
|
+
if isinstance(node, ast.Raise):
|
|
390
|
+
if node.exc:
|
|
391
|
+
# Extract exception name
|
|
392
|
+
if isinstance(node.exc, ast.Name):
|
|
393
|
+
raised_exceptions.add(node.exc.id)
|
|
394
|
+
elif isinstance(node.exc, ast.Call) and isinstance(node.exc.func, ast.Name):
|
|
395
|
+
raised_exceptions.add(node.exc.func.id)
|
|
396
|
+
|
|
397
|
+
# Check if docstring has Raises section
|
|
398
|
+
has_raises_section = 'Raises:' in docstring or 'Raises\n' in docstring or ':raises' in docstring.lower()
|
|
399
|
+
|
|
400
|
+
if raised_exceptions and not has_raises_section:
|
|
401
|
+
issues.append(ValidationIssue(
|
|
402
|
+
severity=Severity.WARNING,
|
|
403
|
+
category="exceptions",
|
|
404
|
+
location=location,
|
|
405
|
+
message=f"Function raises exceptions {raised_exceptions} but has no Raises section",
|
|
406
|
+
suggestion="Add a Raises section documenting the exceptions"
|
|
407
|
+
))
|
|
408
|
+
|
|
409
|
+
return issues
|
|
410
|
+
|
|
411
|
+
def check_return_documentation(self, func_node: Union[ast.FunctionDef, ast.AsyncFunctionDef], docstring: str, location: str) -> List[ValidationIssue]:
|
|
412
|
+
"""
|
|
413
|
+
Validate return value documentation.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
func_node (Union[ast.FunctionDef, ast.AsyncFunctionDef]): The function node.
|
|
417
|
+
docstring (str): The docstring to check.
|
|
418
|
+
location (str): The location string for issues.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
List[ValidationIssue]: List of issues found.
|
|
422
|
+
"""
|
|
423
|
+
issues = []
|
|
424
|
+
parser = DocstringParser(docstring)
|
|
425
|
+
|
|
426
|
+
# Find all return statements
|
|
427
|
+
has_return_value = False
|
|
428
|
+
for node in ast.walk(func_node):
|
|
429
|
+
if isinstance(node, ast.Return) and node.value is not None:
|
|
430
|
+
has_return_value = True
|
|
431
|
+
break
|
|
432
|
+
|
|
433
|
+
# Check consistency
|
|
434
|
+
if has_return_value and not parser.returns:
|
|
435
|
+
issues.append(ValidationIssue(
|
|
436
|
+
severity=Severity.WARNING,
|
|
437
|
+
category="returns",
|
|
438
|
+
location=location,
|
|
439
|
+
message="Function returns a value but has no Returns section in docstring",
|
|
440
|
+
suggestion="Add a Returns section documenting the return value"
|
|
441
|
+
))
|
|
442
|
+
elif not has_return_value and parser.returns and func_node.name != '__init__':
|
|
443
|
+
issues.append(ValidationIssue(
|
|
444
|
+
severity=Severity.INFO,
|
|
445
|
+
category="returns",
|
|
446
|
+
location=location,
|
|
447
|
+
message="Function has Returns section but doesn't return a value",
|
|
448
|
+
suggestion="Remove the Returns section or add a return statement"
|
|
449
|
+
))
|
|
450
|
+
|
|
451
|
+
return issues
|
|
452
|
+
|
|
453
|
+
def check_format_compliance(self, docstring: str, location: str) -> List[ValidationIssue]:
|
|
454
|
+
"""
|
|
455
|
+
Validate docstring follows Google-style format.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
docstring (str): The docstring to check.
|
|
459
|
+
location (str): The location string for issues.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
List[ValidationIssue]: List of issues found.
|
|
463
|
+
"""
|
|
464
|
+
issues = []
|
|
465
|
+
|
|
466
|
+
# Check for summary line
|
|
467
|
+
lines = docstring.strip().splitlines()
|
|
468
|
+
if not lines:
|
|
469
|
+
issues.append(ValidationIssue(
|
|
470
|
+
severity=Severity.ERROR,
|
|
471
|
+
category="format",
|
|
472
|
+
location=location,
|
|
473
|
+
message="Docstring is empty",
|
|
474
|
+
suggestion="Add at least a summary line"
|
|
475
|
+
))
|
|
476
|
+
return issues
|
|
477
|
+
|
|
478
|
+
summary = lines[0].strip()
|
|
479
|
+
if not summary:
|
|
480
|
+
issues.append(ValidationIssue(
|
|
481
|
+
severity=Severity.ERROR,
|
|
482
|
+
category="format",
|
|
483
|
+
location=location,
|
|
484
|
+
message="Docstring missing summary line",
|
|
485
|
+
suggestion="Add a summary line as the first line of the docstring"
|
|
486
|
+
))
|
|
487
|
+
|
|
488
|
+
# Check for proper section headers (Google style)
|
|
489
|
+
valid_sections = ['Args:', 'Returns:', 'Raises:', 'Yields:', 'Attributes:', 'Example:', 'Examples:', 'Note:', 'Notes:']
|
|
490
|
+
for line in lines:
|
|
491
|
+
stripped = line.strip()
|
|
492
|
+
if stripped.endswith(':') and stripped not in valid_sections:
|
|
493
|
+
# Check if it looks like a section header
|
|
494
|
+
if stripped[0].isupper() and len(stripped.split()) == 1:
|
|
495
|
+
issues.append(ValidationIssue(
|
|
496
|
+
severity=Severity.INFO,
|
|
497
|
+
category="format",
|
|
498
|
+
location=location,
|
|
499
|
+
message=f"Non-standard section header '{stripped}' found",
|
|
500
|
+
suggestion=f"Use standard Google-style sections: {', '.join(valid_sections)}"
|
|
501
|
+
))
|
|
502
|
+
|
|
503
|
+
return issues
|
|
504
|
+
|
|
505
|
+
def check_content_quality(self, docstring: str, location: str) -> List[ValidationIssue]:
|
|
506
|
+
"""
|
|
507
|
+
Check for low-quality documentation content.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
docstring (str): The docstring to check.
|
|
511
|
+
location (str): The location string for issues.
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
List[ValidationIssue]: List of issues found.
|
|
515
|
+
"""
|
|
516
|
+
issues = []
|
|
517
|
+
parser = DocstringParser(docstring)
|
|
518
|
+
|
|
519
|
+
# Check for placeholder text
|
|
520
|
+
placeholders = ['TODO', 'FIXME', 'XXX', 'HACK', 'Description of', 'TBD', 'To be determined']
|
|
521
|
+
for placeholder in placeholders:
|
|
522
|
+
if placeholder.lower() in docstring.lower():
|
|
523
|
+
issues.append(ValidationIssue(
|
|
524
|
+
severity=Severity.WARNING,
|
|
525
|
+
category="quality",
|
|
526
|
+
location=location,
|
|
527
|
+
message=f"Placeholder text '{placeholder}' found in docstring",
|
|
528
|
+
suggestion="Replace placeholder with actual documentation"
|
|
529
|
+
))
|
|
530
|
+
|
|
531
|
+
# Check summary length
|
|
532
|
+
if parser.summary and len(parser.summary) < 10:
|
|
533
|
+
issues.append(ValidationIssue(
|
|
534
|
+
severity=Severity.INFO,
|
|
535
|
+
category="quality",
|
|
536
|
+
location=location,
|
|
537
|
+
message=f"Summary line is very short ({len(parser.summary)} chars)",
|
|
538
|
+
suggestion="Provide a more descriptive summary (at least 10 characters)"
|
|
539
|
+
))
|
|
540
|
+
|
|
541
|
+
# Check for duplicate parameter descriptions
|
|
542
|
+
if len(parser.params) > 1:
|
|
543
|
+
descriptions = list(parser.params.values())
|
|
544
|
+
if len(descriptions) != len(set(descriptions)):
|
|
545
|
+
issues.append(ValidationIssue(
|
|
546
|
+
severity=Severity.WARNING,
|
|
547
|
+
category="quality",
|
|
548
|
+
location=location,
|
|
549
|
+
message="Multiple parameters have identical descriptions",
|
|
550
|
+
suggestion="Provide unique descriptions for each parameter"
|
|
551
|
+
))
|
|
552
|
+
|
|
553
|
+
# Check if summary and description are identical
|
|
554
|
+
if parser.summary and parser.description and parser.summary.strip() == parser.description.strip():
|
|
555
|
+
issues.append(ValidationIssue(
|
|
556
|
+
severity=Severity.INFO,
|
|
557
|
+
category="quality",
|
|
558
|
+
location=location,
|
|
559
|
+
message="Summary and description are identical",
|
|
560
|
+
suggestion="Either remove the description or expand it with more details"
|
|
561
|
+
))
|
|
562
|
+
|
|
563
|
+
return issues
|