thailint 0.5.0__py3-none-any.whl → 0.15.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.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Python AST analysis for finding method-should-be-property candidates
|
|
3
|
+
|
|
4
|
+
Scope: Python method detection and property candidacy analysis
|
|
5
|
+
|
|
6
|
+
Overview: Provides PythonMethodAnalyzer class that traverses Python AST to find methods that
|
|
7
|
+
should be converted to @property decorators. Identifies simple accessor methods (returning
|
|
8
|
+
self._attribute), get_* prefixed methods (Java-style), and simple computed values. Implements
|
|
9
|
+
comprehensive exclusion rules to minimize false positives: methods with parameters, side
|
|
10
|
+
effects (assignments, loops, try/except), external function calls, decorators, complex bodies,
|
|
11
|
+
dunder methods, and async definitions. Returns structured data about each candidate including
|
|
12
|
+
method name, class name, line number, and column for violation reporting.
|
|
13
|
+
|
|
14
|
+
Dependencies: ast module for AST parsing and node types, config module for exclusion defaults
|
|
15
|
+
|
|
16
|
+
Exports: PythonMethodAnalyzer class, PropertyCandidate dataclass
|
|
17
|
+
|
|
18
|
+
Interfaces: find_property_candidates(tree) -> list[PropertyCandidate]
|
|
19
|
+
|
|
20
|
+
Implementation: AST walk pattern with comprehensive method body analysis and exclusion checks
|
|
21
|
+
|
|
22
|
+
Suppressions:
|
|
23
|
+
- srp: Analyzer class implements comprehensive exclusion rules requiring many helper methods.
|
|
24
|
+
All methods support single responsibility of property candidate detection.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import ast
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
|
|
30
|
+
from .config import DEFAULT_EXCLUDE_NAMES, DEFAULT_EXCLUDE_PREFIXES
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class PropertyCandidate:
|
|
35
|
+
"""Represents a method that should be a property."""
|
|
36
|
+
|
|
37
|
+
method_name: str
|
|
38
|
+
class_name: str
|
|
39
|
+
line: int
|
|
40
|
+
column: int
|
|
41
|
+
is_get_prefix: bool
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PythonMethodAnalyzer: # thailint: ignore[srp]
|
|
45
|
+
"""Analyzes Python AST to find methods that should be properties."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
max_body_statements: int = 3,
|
|
50
|
+
exclude_prefixes: tuple[str, ...] | None = None,
|
|
51
|
+
exclude_names: frozenset[str] | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Initialize the analyzer.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
max_body_statements: Maximum statements in method body
|
|
57
|
+
exclude_prefixes: Action verb prefixes to exclude (uses defaults if None)
|
|
58
|
+
exclude_names: Action verb names to exclude (uses defaults if None)
|
|
59
|
+
"""
|
|
60
|
+
self.max_body_statements = max_body_statements
|
|
61
|
+
self.exclude_prefixes = exclude_prefixes or DEFAULT_EXCLUDE_PREFIXES
|
|
62
|
+
self.exclude_names = exclude_names or DEFAULT_EXCLUDE_NAMES
|
|
63
|
+
self.candidates: list[PropertyCandidate] = []
|
|
64
|
+
self._visited_classes: set[int] = set()
|
|
65
|
+
|
|
66
|
+
def find_property_candidates(self, tree: ast.AST) -> list[PropertyCandidate]:
|
|
67
|
+
"""Find all methods that should be properties.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
tree: The AST to analyze
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of PropertyCandidate objects
|
|
74
|
+
"""
|
|
75
|
+
self.candidates = []
|
|
76
|
+
self._visit_classes(tree)
|
|
77
|
+
return self.candidates
|
|
78
|
+
|
|
79
|
+
def _visit_classes(self, tree: ast.AST) -> None:
|
|
80
|
+
"""Visit all top-level and nested classes in the AST.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
tree: AST to traverse
|
|
84
|
+
"""
|
|
85
|
+
self._visited_classes.clear()
|
|
86
|
+
self._visit_node(tree)
|
|
87
|
+
|
|
88
|
+
def _visit_node(self, node: ast.AST) -> None:
|
|
89
|
+
"""Visit a node and its children for classes.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
node: AST node to visit
|
|
93
|
+
"""
|
|
94
|
+
if isinstance(node, ast.ClassDef):
|
|
95
|
+
class_id = id(node)
|
|
96
|
+
if class_id not in self._visited_classes:
|
|
97
|
+
self._visited_classes.add(class_id)
|
|
98
|
+
self._analyze_class(node)
|
|
99
|
+
else:
|
|
100
|
+
for child in ast.iter_child_nodes(node):
|
|
101
|
+
self._visit_node(child)
|
|
102
|
+
|
|
103
|
+
def _analyze_class(self, class_node: ast.ClassDef) -> None:
|
|
104
|
+
"""Analyze a class for property candidates.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
class_node: The ClassDef node
|
|
108
|
+
"""
|
|
109
|
+
for item in class_node.body:
|
|
110
|
+
self._process_class_item(item, class_node.name)
|
|
111
|
+
|
|
112
|
+
def _process_class_item(self, item: ast.stmt, class_name: str) -> None:
|
|
113
|
+
"""Process a single item in a class body.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
item: Item in the class body
|
|
117
|
+
class_name: Name of the containing class
|
|
118
|
+
"""
|
|
119
|
+
if isinstance(item, ast.FunctionDef):
|
|
120
|
+
self._check_method(item, class_name)
|
|
121
|
+
elif isinstance(item, ast.ClassDef):
|
|
122
|
+
self._process_nested_class(item)
|
|
123
|
+
|
|
124
|
+
def _process_nested_class(self, class_node: ast.ClassDef) -> None:
|
|
125
|
+
"""Process a nested class, avoiding duplicates.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
class_node: The nested class node
|
|
129
|
+
"""
|
|
130
|
+
class_id = id(class_node)
|
|
131
|
+
if class_id in self._visited_classes:
|
|
132
|
+
return
|
|
133
|
+
self._visited_classes.add(class_id)
|
|
134
|
+
self._analyze_class(class_node)
|
|
135
|
+
|
|
136
|
+
def _check_method(self, method: ast.FunctionDef, class_name: str) -> None:
|
|
137
|
+
"""Check if method should be a property.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
method: The FunctionDef node
|
|
141
|
+
class_name: Name of the containing class
|
|
142
|
+
"""
|
|
143
|
+
if not self._is_property_candidate(method):
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
is_get_prefix = method.name.startswith("get_") and len(method.name) > 4
|
|
147
|
+
candidate = PropertyCandidate(
|
|
148
|
+
method_name=method.name,
|
|
149
|
+
class_name=class_name,
|
|
150
|
+
line=method.lineno,
|
|
151
|
+
column=method.col_offset,
|
|
152
|
+
is_get_prefix=is_get_prefix,
|
|
153
|
+
)
|
|
154
|
+
self.candidates.append(candidate)
|
|
155
|
+
|
|
156
|
+
def _is_property_candidate(self, method: ast.FunctionDef) -> bool:
|
|
157
|
+
"""Check if method should be a property.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
method: The FunctionDef node
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if method is a property candidate
|
|
164
|
+
"""
|
|
165
|
+
# All conditions must be met for property candidacy
|
|
166
|
+
checks = [
|
|
167
|
+
not self._is_dunder_method(method),
|
|
168
|
+
not self._is_action_verb_method(method),
|
|
169
|
+
not self._has_decorators(method),
|
|
170
|
+
self._takes_only_self(method),
|
|
171
|
+
self._has_simple_body(method),
|
|
172
|
+
self._returns_value(method),
|
|
173
|
+
not self._has_side_effects(method),
|
|
174
|
+
not self._has_control_flow(method),
|
|
175
|
+
not self._has_external_calls(method),
|
|
176
|
+
]
|
|
177
|
+
return all(checks)
|
|
178
|
+
|
|
179
|
+
def _is_dunder_method(self, method: ast.FunctionDef) -> bool:
|
|
180
|
+
"""Check if method is a dunder method.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
method: The method node
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
True if dunder method
|
|
187
|
+
"""
|
|
188
|
+
name = method.name
|
|
189
|
+
return name.startswith("__") and name.endswith("__")
|
|
190
|
+
|
|
191
|
+
def _is_action_verb_method(self, method: ast.FunctionDef) -> bool:
|
|
192
|
+
"""Check if method is an action verb (transformation/lifecycle method).
|
|
193
|
+
|
|
194
|
+
Methods like to_dict(), to_json(), finalize() represent actions, not
|
|
195
|
+
property access. These should remain as methods following Python idioms.
|
|
196
|
+
Also handles private method variants like _to_dict(), _generate_html().
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
method: The method node
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
True if method is an action verb
|
|
203
|
+
"""
|
|
204
|
+
name = method.name
|
|
205
|
+
|
|
206
|
+
# Strip leading underscores to handle private method variants
|
|
207
|
+
# e.g., _generate_legend_section should match generate_* pattern
|
|
208
|
+
stripped_name = name.lstrip("_")
|
|
209
|
+
|
|
210
|
+
# Check for action verb prefixes like to_*, generate_*, etc.
|
|
211
|
+
for prefix in self.exclude_prefixes:
|
|
212
|
+
if stripped_name.startswith(prefix) and len(stripped_name) > len(prefix):
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
# Check for specific action verb names (also check stripped version)
|
|
216
|
+
return name in self.exclude_names or stripped_name in self.exclude_names
|
|
217
|
+
|
|
218
|
+
def _has_decorators(self, method: ast.FunctionDef) -> bool:
|
|
219
|
+
"""Check if method has any decorators.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
method: The method node
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if method has decorators
|
|
226
|
+
"""
|
|
227
|
+
return len(method.decorator_list) > 0
|
|
228
|
+
|
|
229
|
+
def _takes_only_self(self, method: ast.FunctionDef) -> bool:
|
|
230
|
+
"""Check if method takes only self parameter.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
method: The method node
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if only self parameter
|
|
237
|
+
"""
|
|
238
|
+
args = method.args
|
|
239
|
+
has_only_self_arg = len(args.args) == 1
|
|
240
|
+
has_extra_args = self._has_extra_args(args)
|
|
241
|
+
return has_only_self_arg and not has_extra_args
|
|
242
|
+
|
|
243
|
+
def _has_extra_args(self, args: ast.arguments) -> bool:
|
|
244
|
+
"""Check if arguments has extra parameters beyond self.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
args: Method arguments node
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
True if extra arguments present
|
|
251
|
+
"""
|
|
252
|
+
has_positional_only = bool(args.posonlyargs)
|
|
253
|
+
has_vararg = args.vararg is not None
|
|
254
|
+
has_keyword_only = bool(args.kwonlyargs)
|
|
255
|
+
has_kwarg = args.kwarg is not None
|
|
256
|
+
has_defaults = bool(args.defaults)
|
|
257
|
+
has_kw_defaults = args.kw_defaults and any(d is not None for d in args.kw_defaults)
|
|
258
|
+
|
|
259
|
+
return any(
|
|
260
|
+
[
|
|
261
|
+
has_positional_only,
|
|
262
|
+
has_vararg,
|
|
263
|
+
has_keyword_only,
|
|
264
|
+
has_kwarg,
|
|
265
|
+
has_defaults,
|
|
266
|
+
has_kw_defaults,
|
|
267
|
+
]
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def _has_simple_body(self, method: ast.FunctionDef) -> bool:
|
|
271
|
+
"""Check if method body is simple (1-3 statements).
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
method: The method node
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
True if body is simple enough
|
|
278
|
+
"""
|
|
279
|
+
# Filter out docstrings
|
|
280
|
+
body = self._get_non_docstring_body(method)
|
|
281
|
+
|
|
282
|
+
# Check statement count
|
|
283
|
+
if len(body) > self.max_body_statements:
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
if len(body) == 0:
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
def _get_non_docstring_body(self, method: ast.FunctionDef) -> list[ast.stmt]:
|
|
292
|
+
"""Get method body excluding docstrings.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
method: The method node
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
List of statements excluding docstrings
|
|
299
|
+
"""
|
|
300
|
+
body = method.body
|
|
301
|
+
if not body:
|
|
302
|
+
return []
|
|
303
|
+
|
|
304
|
+
# Check if first statement is a docstring
|
|
305
|
+
first = body[0]
|
|
306
|
+
if isinstance(first, ast.Expr) and isinstance(first.value, ast.Constant):
|
|
307
|
+
if isinstance(first.value.value, str):
|
|
308
|
+
return body[1:]
|
|
309
|
+
|
|
310
|
+
return body
|
|
311
|
+
|
|
312
|
+
def _returns_value(self, method: ast.FunctionDef) -> bool:
|
|
313
|
+
"""Check if method returns a non-None value.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
method: The method node
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
True if method returns a value
|
|
320
|
+
"""
|
|
321
|
+
body = self._get_non_docstring_body(method)
|
|
322
|
+
if not body:
|
|
323
|
+
return False
|
|
324
|
+
last = body[-1]
|
|
325
|
+
return self._is_value_return(last)
|
|
326
|
+
|
|
327
|
+
def _is_value_return(self, node: ast.stmt) -> bool:
|
|
328
|
+
"""Check if node is a return statement with a non-None value.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
node: Statement node to check
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
True if return statement with value
|
|
335
|
+
"""
|
|
336
|
+
if not isinstance(node, ast.Return):
|
|
337
|
+
return False
|
|
338
|
+
if node.value is None:
|
|
339
|
+
return False
|
|
340
|
+
if isinstance(node.value, ast.Constant) and node.value.value is None:
|
|
341
|
+
return False
|
|
342
|
+
return True
|
|
343
|
+
|
|
344
|
+
def _has_side_effects(self, method: ast.FunctionDef) -> bool:
|
|
345
|
+
"""Check if method has side effects (assignments to self.*).
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
method: The method node
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
True if has side effects
|
|
352
|
+
"""
|
|
353
|
+
return any(self._is_side_effect_node(node) for node in ast.walk(method))
|
|
354
|
+
|
|
355
|
+
def _is_side_effect_node(self, node: ast.AST) -> bool:
|
|
356
|
+
"""Check if a node represents a side effect.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
node: AST node to check
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
True if node is a side effect
|
|
363
|
+
"""
|
|
364
|
+
return (
|
|
365
|
+
self._is_self_assign(node)
|
|
366
|
+
or self._is_self_aug_assign(node)
|
|
367
|
+
or self._is_self_ann_assign(node)
|
|
368
|
+
or self._is_self_delete(node)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def _is_self_assign(self, node: ast.AST) -> bool:
|
|
372
|
+
"""Check if node is assignment to self."""
|
|
373
|
+
return isinstance(node, ast.Assign) and self._assigns_to_self(node.targets)
|
|
374
|
+
|
|
375
|
+
def _is_self_aug_assign(self, node: ast.AST) -> bool:
|
|
376
|
+
"""Check if node is augmented assignment to self."""
|
|
377
|
+
return isinstance(node, ast.AugAssign) and self._is_self_target(node.target)
|
|
378
|
+
|
|
379
|
+
def _is_self_ann_assign(self, node: ast.AST) -> bool:
|
|
380
|
+
"""Check if node is annotated assignment to self."""
|
|
381
|
+
if not isinstance(node, ast.AnnAssign):
|
|
382
|
+
return False
|
|
383
|
+
return node.value is not None and self._is_self_target(node.target)
|
|
384
|
+
|
|
385
|
+
def _is_self_delete(self, node: ast.AST) -> bool:
|
|
386
|
+
"""Check if node is delete of self attribute."""
|
|
387
|
+
return isinstance(node, ast.Delete) and self._assigns_to_self(node.targets)
|
|
388
|
+
|
|
389
|
+
def _assigns_to_self(self, targets: list[ast.expr]) -> bool:
|
|
390
|
+
"""Check if any target is a self attribute.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
targets: Assignment targets
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
True if assigning to self.*
|
|
397
|
+
"""
|
|
398
|
+
return any(self._is_self_target(target) for target in targets)
|
|
399
|
+
|
|
400
|
+
def _is_self_target(self, target: ast.expr) -> bool:
|
|
401
|
+
"""Check if target is a self attribute (self.* or self._*).
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
target: Assignment target
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
True if target is self.*
|
|
408
|
+
"""
|
|
409
|
+
if isinstance(target, ast.Attribute):
|
|
410
|
+
if isinstance(target.value, ast.Name) and target.value.id == "self":
|
|
411
|
+
return True
|
|
412
|
+
return False
|
|
413
|
+
|
|
414
|
+
# Node types that indicate complex control flow
|
|
415
|
+
_CONTROL_FLOW_TYPES: tuple[type, ...] = (
|
|
416
|
+
ast.For,
|
|
417
|
+
ast.While,
|
|
418
|
+
ast.Try,
|
|
419
|
+
ast.If,
|
|
420
|
+
ast.With,
|
|
421
|
+
ast.Raise,
|
|
422
|
+
ast.ListComp,
|
|
423
|
+
ast.DictComp,
|
|
424
|
+
ast.SetComp,
|
|
425
|
+
ast.GeneratorExp,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def _has_control_flow(self, method: ast.FunctionDef) -> bool:
|
|
429
|
+
"""Check if method has complex control flow.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
method: The method node
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
True if has complex control flow
|
|
436
|
+
"""
|
|
437
|
+
return any(isinstance(node, self._CONTROL_FLOW_TYPES) for node in ast.walk(method))
|
|
438
|
+
|
|
439
|
+
def _has_external_calls(self, method: ast.FunctionDef) -> bool:
|
|
440
|
+
"""Check if method has external function calls.
|
|
441
|
+
|
|
442
|
+
External calls are top-level function calls like print(), format_date().
|
|
443
|
+
Method calls on objects like self._name.upper() or v.strip() are OK.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
method: The method node
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
True if has external calls
|
|
450
|
+
"""
|
|
451
|
+
call_nodes = (node for node in ast.walk(method) if isinstance(node, ast.Call))
|
|
452
|
+
return any(self._is_external_function_call(node) for node in call_nodes)
|
|
453
|
+
|
|
454
|
+
def _is_external_function_call(self, call: ast.Call) -> bool:
|
|
455
|
+
"""Check if call is an external function (not a method on an object).
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
call: The Call node
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
True if external function call like print(), format_date()
|
|
462
|
+
"""
|
|
463
|
+
func = call.func
|
|
464
|
+
|
|
465
|
+
# Simple name call like print(), format_date()
|
|
466
|
+
if isinstance(func, ast.Name):
|
|
467
|
+
return True
|
|
468
|
+
|
|
469
|
+
# Method call like obj.method() - these are OK
|
|
470
|
+
if isinstance(func, ast.Attribute):
|
|
471
|
+
return False
|
|
472
|
+
|
|
473
|
+
return False
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Builds Violation objects for method-should-be-property detection
|
|
3
|
+
|
|
4
|
+
Scope: Violation creation for methods that should be @property decorators
|
|
5
|
+
|
|
6
|
+
Overview: Provides ViolationBuilder class that creates Violation objects for method-property
|
|
7
|
+
detections. Generates descriptive messages indicating which methods should be converted to
|
|
8
|
+
@property decorators, with special handling for get_* prefix methods (Java-style) that
|
|
9
|
+
suggests removing the prefix. Constructs complete Violation instances with rule_id,
|
|
10
|
+
file_path, line number, column, message, and suggestions for Pythonic refactoring.
|
|
11
|
+
|
|
12
|
+
Dependencies: pathlib.Path for file paths, src.core.types.Violation for violation structure
|
|
13
|
+
|
|
14
|
+
Exports: ViolationBuilder class
|
|
15
|
+
|
|
16
|
+
Interfaces: create_violation(method_name, line, column, file_path, is_get_prefix, class_name)
|
|
17
|
+
|
|
18
|
+
Implementation: Builder pattern with message templates suggesting @property decorator conversion
|
|
19
|
+
|
|
20
|
+
Suppressions:
|
|
21
|
+
- too-many-arguments,too-many-positional-arguments: Violation creation with related params
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from src.core.types import Violation
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ViolationBuilder:
|
|
30
|
+
"""Builds violations for method-should-be-property detections."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, rule_id: str) -> None:
|
|
33
|
+
"""Initialize the violation builder.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
rule_id: The rule ID to use in violations
|
|
37
|
+
"""
|
|
38
|
+
self.rule_id = rule_id
|
|
39
|
+
|
|
40
|
+
def create_violation( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
41
|
+
self,
|
|
42
|
+
method_name: str,
|
|
43
|
+
line: int,
|
|
44
|
+
column: int,
|
|
45
|
+
file_path: Path | None,
|
|
46
|
+
is_get_prefix: bool = False,
|
|
47
|
+
class_name: str | None = None,
|
|
48
|
+
) -> Violation:
|
|
49
|
+
"""Create a violation for a method that should be a property.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
method_name: Name of the method
|
|
53
|
+
line: Line number where the violation occurs
|
|
54
|
+
column: Column number where the violation occurs
|
|
55
|
+
file_path: Path to the file
|
|
56
|
+
is_get_prefix: Whether method has get_ prefix
|
|
57
|
+
class_name: Optional class name for context
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Violation object with details about the method
|
|
61
|
+
"""
|
|
62
|
+
message = self._build_message(method_name, is_get_prefix, class_name)
|
|
63
|
+
suggestion = self._build_suggestion(method_name, is_get_prefix)
|
|
64
|
+
|
|
65
|
+
return Violation(
|
|
66
|
+
rule_id=self.rule_id,
|
|
67
|
+
file_path=str(file_path) if file_path else "",
|
|
68
|
+
line=line,
|
|
69
|
+
column=column,
|
|
70
|
+
message=message,
|
|
71
|
+
suggestion=suggestion,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _build_message(
|
|
75
|
+
self,
|
|
76
|
+
method_name: str,
|
|
77
|
+
is_get_prefix: bool,
|
|
78
|
+
class_name: str | None,
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Build the violation message.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
method_name: Name of the method
|
|
84
|
+
is_get_prefix: Whether method has get_ prefix
|
|
85
|
+
class_name: Optional class name
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Human-readable message describing the violation
|
|
89
|
+
"""
|
|
90
|
+
if is_get_prefix:
|
|
91
|
+
property_name = method_name[4:] # Remove 'get_' prefix
|
|
92
|
+
if class_name:
|
|
93
|
+
return (
|
|
94
|
+
f"Method '{method_name}' in class '{class_name}' should be "
|
|
95
|
+
f"a @property named '{property_name}'"
|
|
96
|
+
)
|
|
97
|
+
return f"Method '{method_name}' should be a @property named '{property_name}'"
|
|
98
|
+
|
|
99
|
+
if class_name:
|
|
100
|
+
return f"Method '{method_name}' in class '{class_name}' should be a @property"
|
|
101
|
+
return f"Method '{method_name}' should be a @property"
|
|
102
|
+
|
|
103
|
+
def _build_suggestion(self, method_name: str, is_get_prefix: bool) -> str:
|
|
104
|
+
"""Build the suggestion for fixing the violation.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
method_name: Name of the method
|
|
108
|
+
is_get_prefix: Whether method has get_ prefix
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Actionable suggestion for fixing
|
|
112
|
+
"""
|
|
113
|
+
if is_get_prefix:
|
|
114
|
+
property_name = method_name[4:] # Remove 'get_' prefix
|
|
115
|
+
return (
|
|
116
|
+
f"Add @property decorator and rename to '{property_name}' "
|
|
117
|
+
f"for Pythonic attribute access"
|
|
118
|
+
)
|
|
119
|
+
return "Add @property decorator for Pythonic attribute access"
|
src/linters/nesting/linter.py
CHANGED
|
@@ -16,15 +16,15 @@ Exports: NestingDepthRule class
|
|
|
16
16
|
Interfaces: NestingDepthRule.check(context) -> list[Violation], properties for rule metadata
|
|
17
17
|
|
|
18
18
|
Implementation: Composition pattern with helper classes, AST-based analysis with configurable limits
|
|
19
|
+
|
|
19
20
|
"""
|
|
20
21
|
|
|
21
|
-
import ast
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
24
24
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
25
|
-
from src.core.linter_utils import load_linter_config
|
|
25
|
+
from src.core.linter_utils import load_linter_config, with_parsed_python
|
|
26
26
|
from src.core.types import Violation
|
|
27
|
-
from src.linter_config.ignore import
|
|
27
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
28
28
|
|
|
29
29
|
from .config import NestingConfig
|
|
30
30
|
from .python_analyzer import PythonNestingAnalyzer
|
|
@@ -37,8 +37,11 @@ class NestingDepthRule(MultiLanguageLintRule):
|
|
|
37
37
|
|
|
38
38
|
def __init__(self) -> None:
|
|
39
39
|
"""Initialize the nesting depth rule."""
|
|
40
|
-
self._ignore_parser =
|
|
40
|
+
self._ignore_parser = get_ignore_parser()
|
|
41
41
|
self._violation_builder = NestingViolationBuilder(self.rule_id)
|
|
42
|
+
# Singleton analyzers for performance (avoid recreating per-file)
|
|
43
|
+
self._python_analyzer = PythonNestingAnalyzer()
|
|
44
|
+
self._typescript_analyzer = TypeScriptNestingAnalyzer()
|
|
42
45
|
|
|
43
46
|
@property
|
|
44
47
|
def rule_id(self) -> str:
|
|
@@ -103,14 +106,18 @@ class NestingDepthRule(MultiLanguageLintRule):
|
|
|
103
106
|
Returns:
|
|
104
107
|
List of violations found in Python code
|
|
105
108
|
"""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
return with_parsed_python(
|
|
110
|
+
context,
|
|
111
|
+
self._violation_builder,
|
|
112
|
+
lambda tree: self._analyze_python_tree(tree, config, context),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _analyze_python_tree(
|
|
116
|
+
self, tree: Any, config: NestingConfig, context: BaseLintContext
|
|
117
|
+
) -> list[Violation]:
|
|
118
|
+
"""Analyze parsed Python AST for nesting violations."""
|
|
119
|
+
functions = self._python_analyzer.find_all_functions(tree)
|
|
120
|
+
return self._process_python_functions(functions, self._python_analyzer, config, context)
|
|
114
121
|
|
|
115
122
|
def _process_typescript_functions(
|
|
116
123
|
self, functions: list, analyzer: Any, config: NestingConfig, context: BaseLintContext
|
|
@@ -149,13 +156,14 @@ class NestingDepthRule(MultiLanguageLintRule):
|
|
|
149
156
|
Returns:
|
|
150
157
|
List of violations found in TypeScript code
|
|
151
158
|
"""
|
|
152
|
-
|
|
153
|
-
root_node = analyzer.parse_typescript(context.file_content or "")
|
|
159
|
+
root_node = self._typescript_analyzer.parse_typescript(context.file_content or "")
|
|
154
160
|
if root_node is None:
|
|
155
161
|
return []
|
|
156
162
|
|
|
157
|
-
functions =
|
|
158
|
-
return self._process_typescript_functions(
|
|
163
|
+
functions = self._typescript_analyzer.find_all_functions(root_node)
|
|
164
|
+
return self._process_typescript_functions(
|
|
165
|
+
functions, self._typescript_analyzer, config, context
|
|
166
|
+
)
|
|
159
167
|
|
|
160
168
|
def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
161
169
|
"""Check if violation should be ignored based on inline directives.
|
|
@@ -25,6 +25,10 @@ import ast
|
|
|
25
25
|
class PythonNestingAnalyzer:
|
|
26
26
|
"""Calculates maximum nesting depth in Python functions."""
|
|
27
27
|
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
"""Initialize the Python nesting analyzer."""
|
|
30
|
+
pass # Stateless analyzer for nesting depth calculation
|
|
31
|
+
|
|
28
32
|
def calculate_max_depth(
|
|
29
33
|
self, func_node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
30
34
|
) -> tuple[int, int]:
|