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.
Files changed (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {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"
@@ -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 IgnoreDirectiveParser
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 = IgnoreDirectiveParser()
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
- try:
107
- tree = ast.parse(context.file_content or "")
108
- except SyntaxError as e:
109
- return [self._violation_builder.create_syntax_error_violation(e, context)]
110
-
111
- analyzer = PythonNestingAnalyzer()
112
- functions = analyzer.find_all_functions(tree)
113
- return self._process_python_functions(functions, analyzer, config, context)
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
- analyzer = TypeScriptNestingAnalyzer()
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 = analyzer.find_all_functions(root_node)
158
- return self._process_typescript_functions(functions, analyzer, config, context)
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]: