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,417 @@
1
+ """
2
+ Purpose: Detects single-statement patterns in Python code for DRY linter filtering
3
+
4
+ Scope: AST-based analysis to identify single logical statements that should not be flagged as duplicates
5
+
6
+ Overview: Provides sophisticated single-statement pattern detection to filter false positives in the
7
+ DRY linter. Analyzes Python AST to identify when a code block represents a single logical
8
+ statement (class field definitions, decorated functions, multi-line calls, assignments) that
9
+ should not be flagged as duplicate code. Uses line-to-node indexing for O(1) lookups and
10
+ supports various Python language constructs including classes, functions, decorators, and
11
+ nested structures.
12
+
13
+ Dependencies: ast module for Python AST parsing
14
+
15
+ Exports: SingleStatementDetector class
16
+
17
+ Interfaces: SingleStatementDetector.is_single_statement(content, start_line, end_line) -> bool
18
+
19
+ Implementation: AST walking with line-to-node index optimization for performance
20
+
21
+ Suppressions:
22
+ - type:ignore[attr-defined]: Tree-sitter Node.text attribute access (optional dependency)
23
+ - type:ignore[operator]: Tree-sitter Node comparison operations (optional dependency)
24
+ - too-many-arguments,too-many-positional-arguments: Builder pattern with related params
25
+ - srp.violation: Complex AST analysis algorithm for single-statement detection. See SRP Exception below.
26
+
27
+ SRP Exception: SingleStatementDetector has 33 methods and 308 lines (exceeds max 8 methods/200 lines)
28
+ Justification: Complex AST analysis algorithm for single-statement pattern detection with sophisticated
29
+ false positive filtering. Methods form tightly coupled algorithm pipeline: class field detection,
30
+ decorator handling, function call analysis, assignment patterns, and context-aware filtering. Similar
31
+ to parser or compiler pass architecture where algorithmic cohesion is critical. Splitting would
32
+ fragment the algorithm logic and make maintenance harder by separating interdependent AST analysis
33
+ steps. All methods contribute to single responsibility: accurately detecting single-statement patterns
34
+ to prevent false positives in duplicate code detection.
35
+ """
36
+
37
+ import ast
38
+ from collections.abc import Callable
39
+ from typing import cast
40
+
41
+ # AST context checking constants
42
+ AST_LOOKBACK_LINES = 10
43
+ AST_LOOKFORWARD_LINES = 5
44
+
45
+ # Type alias for AST nodes with line number attributes
46
+ ASTWithLineNumbers = ast.stmt | ast.expr
47
+
48
+
49
+ class SingleStatementDetector: # thailint: ignore[srp.violation]
50
+ """Detects single-statement patterns in Python code for duplicate filtering.
51
+
52
+ SRP suppression: Complex AST analysis algorithm requires 33 methods to implement
53
+ sophisticated single-statement detection with false positive filtering. See file header for justification.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ cached_ast: ast.Module | None = None,
59
+ cached_content: str | None = None,
60
+ line_to_nodes: dict[int, list[ast.AST]] | None = None,
61
+ ):
62
+ """Initialize detector with optional cached AST data.
63
+
64
+ Args:
65
+ cached_ast: Pre-parsed AST tree (for performance)
66
+ cached_content: Content that was parsed into cached_ast
67
+ line_to_nodes: Pre-built line-to-node index
68
+ """
69
+ self._cached_ast = cached_ast
70
+ self._cached_content = cached_content
71
+ self._line_to_nodes = line_to_nodes
72
+
73
+ def is_single_statement(self, content: str, start_line: int, end_line: int) -> bool:
74
+ """Check if a line range in the original source is a single logical statement.
75
+
76
+ Performance optimization: Uses cached AST if available to avoid re-parsing.
77
+
78
+ Args:
79
+ content: Source code content
80
+ start_line: Starting line number (1-indexed)
81
+ end_line: Ending line number (1-indexed)
82
+
83
+ Returns:
84
+ True if range represents a single logical statement
85
+ """
86
+ tree = self._get_ast_tree(content)
87
+ if tree is None:
88
+ return False
89
+
90
+ return self._check_overlapping_nodes(tree, start_line, end_line)
91
+
92
+ def _get_ast_tree(self, content: str) -> ast.Module | None:
93
+ """Get AST tree, using cache if available."""
94
+ if self._cached_ast is not None and content == self._cached_content:
95
+ return self._cached_ast
96
+ return self._parse_content_safe(content)
97
+
98
+ @staticmethod
99
+ def _parse_content_safe(content: str) -> ast.Module | None:
100
+ """Parse content, returning None on syntax error."""
101
+ try:
102
+ return ast.parse(content)
103
+ except SyntaxError:
104
+ return None
105
+
106
+ @staticmethod
107
+ def build_line_to_node_index(tree: ast.Module | None) -> dict[int, list[ast.AST]] | None:
108
+ """Build an index mapping each line number to overlapping AST nodes.
109
+
110
+ Performance optimization: Allows O(1) lookups instead of O(n) ast.walk() calls.
111
+
112
+ Args:
113
+ tree: Parsed AST tree (None if parsing failed)
114
+
115
+ Returns:
116
+ Dictionary mapping line numbers to list of AST nodes overlapping that line
117
+ """
118
+ if tree is None:
119
+ return None
120
+
121
+ line_to_nodes: dict[int, list[ast.AST]] = {}
122
+ for node in ast.walk(tree):
123
+ if SingleStatementDetector._node_has_line_info(node):
124
+ SingleStatementDetector._add_node_to_index(node, line_to_nodes)
125
+
126
+ return line_to_nodes
127
+
128
+ @staticmethod
129
+ def _node_has_line_info(node: ast.AST) -> bool:
130
+ """Check if node has valid line number information."""
131
+ if not hasattr(node, "lineno") or not hasattr(node, "end_lineno"):
132
+ return False
133
+ return node.lineno is not None and node.end_lineno is not None
134
+
135
+ @staticmethod
136
+ def _add_node_to_index(node: ast.AST, line_to_nodes: dict[int, list[ast.AST]]) -> None:
137
+ """Add node to all lines it overlaps in the index."""
138
+ for line_num in range(node.lineno, node.end_lineno + 1): # type: ignore[attr-defined]
139
+ if line_num not in line_to_nodes:
140
+ line_to_nodes[line_num] = []
141
+ line_to_nodes[line_num].append(node)
142
+
143
+ def _check_overlapping_nodes(self, tree: ast.Module, start_line: int, end_line: int) -> bool:
144
+ """Check if any AST node overlaps and matches single-statement pattern."""
145
+ if self._line_to_nodes is not None:
146
+ return self._check_nodes_via_index(start_line, end_line)
147
+ return self._check_nodes_via_walk(tree, start_line, end_line)
148
+
149
+ def _check_nodes_via_index(self, start_line: int, end_line: int) -> bool:
150
+ """Check nodes using line-to-node index for O(1) lookups."""
151
+ candidates = self._collect_candidate_nodes(start_line, end_line)
152
+ return self._any_node_matches_pattern(candidates, start_line, end_line)
153
+
154
+ def _collect_candidate_nodes(self, start_line: int, end_line: int) -> set[ast.AST]:
155
+ """Collect unique nodes that overlap with the line range from index."""
156
+ candidate_nodes: set[ast.AST] = set()
157
+ for line_num in range(start_line, end_line + 1):
158
+ if self._line_to_nodes and line_num in self._line_to_nodes:
159
+ candidate_nodes.update(self._line_to_nodes[line_num])
160
+ return candidate_nodes
161
+
162
+ def _any_node_matches_pattern(
163
+ self, nodes: set[ast.AST], start_line: int, end_line: int
164
+ ) -> bool:
165
+ """Check if any node matches single-statement pattern."""
166
+ return any(self._is_single_statement_pattern(node, start_line, end_line) for node in nodes)
167
+
168
+ def _check_nodes_via_walk(self, tree: ast.Module, start_line: int, end_line: int) -> bool:
169
+ """Check nodes using ast.walk() fallback."""
170
+ return any(
171
+ self._node_matches_via_walk(node, start_line, end_line) for node in ast.walk(tree)
172
+ )
173
+
174
+ def _node_matches_via_walk(self, node: ast.AST, start_line: int, end_line: int) -> bool:
175
+ """Check if a single node overlaps and matches pattern."""
176
+ if not self._node_overlaps_range(node, start_line, end_line):
177
+ return False
178
+ return self._is_single_statement_pattern(node, start_line, end_line)
179
+
180
+ @staticmethod
181
+ def _node_overlaps_range(node: ast.AST, start_line: int, end_line: int) -> bool:
182
+ """Check if node overlaps with the given line range."""
183
+ if not hasattr(node, "lineno") or not hasattr(node, "end_lineno"):
184
+ return False
185
+ node_end = node.end_lineno
186
+ node_start = node.lineno
187
+ return not (node_end < start_line or node_start > end_line)
188
+
189
+ def _is_single_statement_pattern(self, node: ast.AST, start_line: int, end_line: int) -> bool:
190
+ """Check if an AST node represents a single-statement pattern to filter."""
191
+ contains = self._node_contains_range(node, start_line, end_line)
192
+ if contains is None:
193
+ return False
194
+
195
+ return self._dispatch_pattern_check(node, start_line, end_line, contains)
196
+
197
+ def _node_contains_range(self, node: ast.AST, start_line: int, end_line: int) -> bool | None:
198
+ """Check if node completely contains the range. Returns None if invalid."""
199
+ if not self._has_valid_line_numbers(node):
200
+ return None
201
+ typed_node = cast(ASTWithLineNumbers, node)
202
+ return typed_node.lineno <= start_line and typed_node.end_lineno >= end_line # type: ignore[operator]
203
+
204
+ @staticmethod
205
+ def _has_valid_line_numbers(node: ast.AST) -> bool:
206
+ """Check if node has valid line number attributes."""
207
+ if not (hasattr(node, "lineno") and hasattr(node, "end_lineno")):
208
+ return False
209
+ return node.lineno is not None and node.end_lineno is not None
210
+
211
+ def _dispatch_pattern_check(
212
+ self, node: ast.AST, start_line: int, end_line: int, contains: bool
213
+ ) -> bool:
214
+ """Dispatch to node-type-specific pattern checkers."""
215
+ if isinstance(node, ast.Expr):
216
+ return contains
217
+
218
+ return self._check_specific_pattern(node, start_line, end_line, contains)
219
+
220
+ def _check_specific_pattern(
221
+ self, node: ast.AST, start_line: int, end_line: int, contains: bool
222
+ ) -> bool:
223
+ """Check specific node types with their pattern rules."""
224
+ if isinstance(node, ast.ClassDef):
225
+ return self._check_class_def_pattern(node, start_line, end_line)
226
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
227
+ return self._check_function_def_pattern(node, start_line, end_line)
228
+ if isinstance(node, ast.Call):
229
+ return self._check_call_pattern(node, start_line, end_line, contains)
230
+ if isinstance(node, ast.Assign):
231
+ return self._check_assign_pattern(node, start_line, end_line, contains)
232
+ return False
233
+
234
+ def _check_class_def_pattern(self, node: ast.ClassDef, start_line: int, end_line: int) -> bool:
235
+ """Check if range is in class field definitions (not method bodies)."""
236
+ first_method_line = self._find_first_method_line(node)
237
+ class_start = self._get_class_start_with_decorators(node)
238
+ return self._is_in_class_fields_area(
239
+ class_start, start_line, end_line, first_method_line, node.end_lineno
240
+ )
241
+
242
+ @staticmethod
243
+ def _find_first_method_line(node: ast.ClassDef) -> int | None:
244
+ """Find line number of first method in class."""
245
+ for item in node.body:
246
+ if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
247
+ return item.lineno
248
+ return None
249
+
250
+ @staticmethod
251
+ def _get_class_start_with_decorators(node: ast.ClassDef) -> int:
252
+ """Get class start line, including decorators if present."""
253
+ if node.decorator_list:
254
+ return min(d.lineno for d in node.decorator_list)
255
+ return node.lineno
256
+
257
+ @staticmethod
258
+ def _is_in_class_fields_area(
259
+ class_start: int,
260
+ start_line: int,
261
+ end_line: int,
262
+ first_method_line: int | None,
263
+ class_end_line: int | None,
264
+ ) -> bool:
265
+ """Check if range is in class fields area (before methods)."""
266
+ if first_method_line is not None:
267
+ return class_start <= start_line and end_line < first_method_line
268
+ if class_end_line is not None:
269
+ return class_start <= start_line and class_end_line >= end_line
270
+ return False
271
+
272
+ def _check_function_def_pattern(
273
+ self, node: ast.FunctionDef | ast.AsyncFunctionDef, start_line: int, end_line: int
274
+ ) -> bool:
275
+ """Check if range is in function decorator pattern."""
276
+ if not node.decorator_list:
277
+ return False
278
+
279
+ first_decorator_line = min(d.lineno for d in node.decorator_list)
280
+ first_body_line = self._get_function_body_start(node)
281
+
282
+ if first_body_line is None:
283
+ return False
284
+
285
+ return start_line >= first_decorator_line and end_line < first_body_line
286
+
287
+ @staticmethod
288
+ def _get_function_body_start(node: ast.FunctionDef | ast.AsyncFunctionDef) -> int | None:
289
+ """Get the line number where function body starts."""
290
+ if not node.body or not hasattr(node.body[0], "lineno"):
291
+ return None
292
+ return node.body[0].lineno
293
+
294
+ def _check_call_pattern(
295
+ self, node: ast.Call, start_line: int, end_line: int, contains: bool
296
+ ) -> bool:
297
+ """Check if range is part of a function/constructor call."""
298
+ return self._check_multiline_or_contained(node, start_line, end_line, contains)
299
+
300
+ def _check_assign_pattern(
301
+ self, node: ast.Assign, start_line: int, end_line: int, contains: bool
302
+ ) -> bool:
303
+ """Check if range is part of a multi-line assignment."""
304
+ return self._check_multiline_or_contained(node, start_line, end_line, contains)
305
+
306
+ def _check_multiline_or_contained(
307
+ self, node: ast.AST, start_line: int, end_line: int, contains: bool
308
+ ) -> bool:
309
+ """Check if node is multiline containing start, or single-line containing range."""
310
+ if not self._has_valid_line_numbers(node):
311
+ return False
312
+
313
+ typed_node = cast(ASTWithLineNumbers, node)
314
+ is_multiline = typed_node.lineno < typed_node.end_lineno # type: ignore[operator]
315
+ if is_multiline:
316
+ return typed_node.lineno <= start_line <= typed_node.end_lineno # type: ignore[operator]
317
+ return contains
318
+
319
+ def is_standalone_single_statement(
320
+ self, lines: list[str], start_line: int, end_line: int
321
+ ) -> bool:
322
+ """Check if the exact range parses as a single statement on its own."""
323
+ source_lines = lines[start_line - 1 : end_line]
324
+ source_snippet = "\n".join(source_lines)
325
+
326
+ try:
327
+ tree = ast.parse(source_snippet)
328
+ return len(tree.body) == 1
329
+ except SyntaxError:
330
+ return False
331
+
332
+ def check_ast_context( # pylint: disable=too-many-arguments,too-many-positional-arguments
333
+ self,
334
+ lines: list[str],
335
+ start_line: int,
336
+ end_line: int,
337
+ lookback: int,
338
+ lookforward: int,
339
+ predicate: Callable[[ast.Module, int], bool],
340
+ ) -> bool:
341
+ """Generic helper for AST-based context checking.
342
+
343
+ Args:
344
+ lines: Source file lines
345
+ start_line: Starting line number (1-indexed)
346
+ end_line: Ending line number (1-indexed)
347
+ lookback: Number of lines to look backward
348
+ lookforward: Number of lines to look forward
349
+ predicate: Function that takes AST tree and lookback_start, returns bool
350
+
351
+ Returns:
352
+ True if predicate returns True for the parsed context
353
+ """
354
+ lookback_start = max(0, start_line - lookback)
355
+ lookforward_end = min(len(lines), end_line + lookforward)
356
+
357
+ context_lines = lines[lookback_start:lookforward_end]
358
+ context = "\n".join(context_lines)
359
+
360
+ try:
361
+ tree = ast.parse(context)
362
+ return predicate(tree, lookback_start)
363
+ except SyntaxError:
364
+ pass
365
+
366
+ return False
367
+
368
+ def is_part_of_decorator(self, lines: list[str], start_line: int, end_line: int) -> bool:
369
+ """Check if lines are part of a decorator + function definition."""
370
+
371
+ def has_decorators(tree: ast.Module, _lookback_start: int) -> bool:
372
+ """Check if any function or class in the tree has decorators."""
373
+ return any(
374
+ isinstance(stmt, (ast.FunctionDef, ast.ClassDef)) and stmt.decorator_list
375
+ for stmt in tree.body
376
+ )
377
+
378
+ return self.check_ast_context(lines, start_line, end_line, 10, 10, has_decorators)
379
+
380
+ def is_part_of_function_call(self, lines: list[str], start_line: int, end_line: int) -> bool:
381
+ """Check if lines are arguments inside a function/constructor call."""
382
+
383
+ def is_single_non_function_statement(tree: ast.Module, _lookback_start: int) -> bool:
384
+ """Check if context has exactly one statement that's not a function/class def."""
385
+ return len(tree.body) == 1 and not isinstance(
386
+ tree.body[0], (ast.FunctionDef, ast.ClassDef)
387
+ )
388
+
389
+ return self.check_ast_context(
390
+ lines, start_line, end_line, 10, 10, is_single_non_function_statement
391
+ )
392
+
393
+ def is_part_of_class_body(self, lines: list[str], start_line: int, end_line: int) -> bool:
394
+ """Check if lines are field definitions inside a class body."""
395
+
396
+ def is_within_class_body(tree: ast.Module, lookback_start: int) -> bool:
397
+ """Check if flagged range falls within a class body."""
398
+ class_defs = (s for s in tree.body if isinstance(s, ast.ClassDef))
399
+ for stmt in class_defs:
400
+ class_start_in_context = stmt.lineno
401
+ class_end_in_context = stmt.end_lineno if stmt.end_lineno else stmt.lineno
402
+
403
+ class_start_original = lookback_start + class_start_in_context
404
+ class_end_original = lookback_start + class_end_in_context
405
+
406
+ if start_line >= class_start_original and end_line <= class_end_original:
407
+ return True
408
+ return False
409
+
410
+ return self.check_ast_context(
411
+ lines,
412
+ start_line,
413
+ end_line,
414
+ AST_LOOKBACK_LINES,
415
+ AST_LOOKFORWARD_LINES,
416
+ is_within_class_body,
417
+ )