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,402 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Analyze filter-map and takewhile anti-patterns in for loops
|
|
3
|
+
|
|
4
|
+
Scope: Extract and validate loops that build lists with transform/filter or takewhile
|
|
5
|
+
|
|
6
|
+
Overview: Provides helper functions for analyzing loops that initialize an empty list,
|
|
7
|
+
iterate with a transform and conditional append, then return the list. Detects
|
|
8
|
+
patterns like 'result=[]; for x: y=f(x); if y: result.append(y); return result'
|
|
9
|
+
which can be refactored to list comprehensions with walrus operator. Also handles
|
|
10
|
+
takewhile patterns with break statements.
|
|
11
|
+
|
|
12
|
+
Dependencies: ast module for Python AST processing
|
|
13
|
+
|
|
14
|
+
Exports: extract_filter_map_pattern, extract_takewhile_pattern, FilterMapMatch, TakewhileMatch
|
|
15
|
+
|
|
16
|
+
Interfaces: Functions for analyzing filter-map/takewhile patterns in AST function bodies
|
|
17
|
+
|
|
18
|
+
Implementation: AST-based pattern matching for filter-map/takewhile pattern identification
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import ast
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
from . import ast_utils
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class FilterMapMatch:
|
|
29
|
+
"""Information about a detected filter-map pattern."""
|
|
30
|
+
|
|
31
|
+
for_node: ast.For
|
|
32
|
+
"""The for loop AST node."""
|
|
33
|
+
|
|
34
|
+
result_var: str
|
|
35
|
+
"""Name of the result list variable."""
|
|
36
|
+
|
|
37
|
+
transform_var: str
|
|
38
|
+
"""Name of the variable holding transform result."""
|
|
39
|
+
|
|
40
|
+
transform_expr: str
|
|
41
|
+
"""The transform expression as string."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class TakewhileMatch:
|
|
46
|
+
"""Information about a detected takewhile pattern."""
|
|
47
|
+
|
|
48
|
+
for_node: ast.For
|
|
49
|
+
"""The for loop AST node."""
|
|
50
|
+
|
|
51
|
+
result_var: str
|
|
52
|
+
"""Name of the result list variable."""
|
|
53
|
+
|
|
54
|
+
condition: ast.expr
|
|
55
|
+
"""The condition expression (inverted from break condition)."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def extract_filter_map_pattern(
|
|
59
|
+
func_body: list[ast.stmt], for_node: ast.For
|
|
60
|
+
) -> FilterMapMatch | None:
|
|
61
|
+
"""Extract filter-map pattern from a for loop in a function body.
|
|
62
|
+
|
|
63
|
+
Pattern: result=[]; for x: y=f(x); if y: result.append(y); return result
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
func_body: List of statements in the function body
|
|
67
|
+
for_node: The for loop AST node to analyze
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
FilterMapMatch if pattern detected, None otherwise
|
|
71
|
+
"""
|
|
72
|
+
# Find the for loop position
|
|
73
|
+
for_index = _get_stmt_index(func_body, for_node)
|
|
74
|
+
if for_index is None:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
# Find result list initialization before the loop
|
|
78
|
+
result_init = _find_result_init_before(func_body, for_index)
|
|
79
|
+
if result_init is None:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
result_var, _ = result_init
|
|
83
|
+
|
|
84
|
+
# Check loop body has: assign, if, append pattern
|
|
85
|
+
loop_pattern = _extract_assign_if_append(for_node.body, result_var)
|
|
86
|
+
if loop_pattern is None:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
transform_var, transform_expr = loop_pattern
|
|
90
|
+
|
|
91
|
+
# Check return result after loop
|
|
92
|
+
if not _is_return_var_after(func_body, for_index, result_var):
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
return FilterMapMatch(
|
|
96
|
+
for_node=for_node,
|
|
97
|
+
result_var=result_var,
|
|
98
|
+
transform_var=transform_var,
|
|
99
|
+
transform_expr=transform_expr,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def extract_takewhile_pattern(
|
|
104
|
+
func_body: list[ast.stmt], for_node: ast.For
|
|
105
|
+
) -> TakewhileMatch | None:
|
|
106
|
+
"""Extract takewhile pattern from a for loop in a function body.
|
|
107
|
+
|
|
108
|
+
Pattern: result=[]; for x: if not cond: break; result.append(x); return result
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
func_body: List of statements in the function body
|
|
112
|
+
for_node: The for loop AST node to analyze
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
TakewhileMatch if pattern detected, None otherwise
|
|
116
|
+
"""
|
|
117
|
+
# Find the for loop position
|
|
118
|
+
for_index = _get_stmt_index(func_body, for_node)
|
|
119
|
+
if for_index is None:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
# Find result list initialization before the loop
|
|
123
|
+
result_init = _find_result_init_before(func_body, for_index)
|
|
124
|
+
if result_init is None:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
result_var, _ = result_init
|
|
128
|
+
|
|
129
|
+
# Check loop body has: if break, append pattern
|
|
130
|
+
loop_pattern = _extract_if_break_append(for_node.body, result_var)
|
|
131
|
+
if loop_pattern is None:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
condition = loop_pattern
|
|
135
|
+
|
|
136
|
+
# Check return result after loop
|
|
137
|
+
if not _is_return_var_after(func_body, for_index, result_var):
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
return TakewhileMatch(
|
|
141
|
+
for_node=for_node,
|
|
142
|
+
result_var=result_var,
|
|
143
|
+
condition=condition,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _get_stmt_index(func_body: list[ast.stmt], target: ast.stmt) -> int | None:
|
|
148
|
+
"""Find index of a statement in a function body."""
|
|
149
|
+
for i, stmt in enumerate(func_body):
|
|
150
|
+
if stmt is target:
|
|
151
|
+
return i
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _get_assign_empty_list_var(stmt: ast.Assign) -> str | None:
|
|
156
|
+
"""Get variable name from simple assignment to empty list."""
|
|
157
|
+
if len(stmt.targets) != 1:
|
|
158
|
+
return None
|
|
159
|
+
if not isinstance(stmt.targets[0], ast.Name):
|
|
160
|
+
return None
|
|
161
|
+
if not _is_empty_list(stmt.value):
|
|
162
|
+
return None
|
|
163
|
+
return stmt.targets[0].id
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _get_annassign_empty_list_var(stmt: ast.AnnAssign) -> str | None:
|
|
167
|
+
"""Get variable name from annotated assignment to empty list."""
|
|
168
|
+
if not isinstance(stmt.target, ast.Name):
|
|
169
|
+
return None
|
|
170
|
+
if stmt.value is None:
|
|
171
|
+
return None
|
|
172
|
+
if not _is_empty_list(stmt.value):
|
|
173
|
+
return None
|
|
174
|
+
return stmt.target.id
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _get_empty_list_var(stmt: ast.stmt) -> str | None:
|
|
178
|
+
"""Get variable name if statement is empty list initialization.
|
|
179
|
+
|
|
180
|
+
Handles both: result = [] and result: list[T] = []
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
stmt: Statement to check
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Variable name if empty list init, None otherwise
|
|
187
|
+
"""
|
|
188
|
+
if isinstance(stmt, ast.Assign):
|
|
189
|
+
return _get_assign_empty_list_var(stmt)
|
|
190
|
+
if isinstance(stmt, ast.AnnAssign):
|
|
191
|
+
return _get_annassign_empty_list_var(stmt)
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _find_result_init_before(func_body: list[ast.stmt], for_index: int) -> tuple[str, int] | None:
|
|
196
|
+
"""Find empty list initialization before the for loop.
|
|
197
|
+
|
|
198
|
+
Searches backwards from for_index to find: result = [] or result: list[T] = []
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
func_body: List of statements in function body
|
|
202
|
+
for_index: Index of the for loop
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Tuple of (variable_name, init_index) if found, None otherwise
|
|
206
|
+
"""
|
|
207
|
+
for i in range(for_index - 1, -1, -1):
|
|
208
|
+
var_name = _get_empty_list_var(func_body[i])
|
|
209
|
+
if var_name is not None:
|
|
210
|
+
return (var_name, i)
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _is_empty_list(node: ast.expr) -> bool:
|
|
215
|
+
"""Check if expression is an empty list literal."""
|
|
216
|
+
return isinstance(node, ast.List) and len(node.elts) == 0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _extract_simple_assignment(stmt: ast.stmt) -> tuple[str, str] | None:
|
|
220
|
+
"""Extract variable and expression from a simple assignment.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
stmt: Statement to check
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Tuple of (var_name, expr_string) if simple assignment, None otherwise
|
|
227
|
+
"""
|
|
228
|
+
if not isinstance(stmt, ast.Assign):
|
|
229
|
+
return None
|
|
230
|
+
if len(stmt.targets) != 1:
|
|
231
|
+
return None
|
|
232
|
+
if not isinstance(stmt.targets[0], ast.Name):
|
|
233
|
+
return None
|
|
234
|
+
return (stmt.targets[0].id, ast.unparse(stmt.value))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _is_conditional_append(
|
|
238
|
+
if_stmt: ast.If, condition_var: str, result_var: str, appended_var: str
|
|
239
|
+
) -> bool:
|
|
240
|
+
"""Check if statement is: if condition_var: result.append(appended_var).
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
if_stmt: The if statement to check
|
|
244
|
+
condition_var: Expected condition variable name
|
|
245
|
+
result_var: Expected result list name
|
|
246
|
+
appended_var: Expected appended variable name
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
True if pattern matches
|
|
250
|
+
"""
|
|
251
|
+
if if_stmt.orelse:
|
|
252
|
+
return False
|
|
253
|
+
if not isinstance(if_stmt.test, ast.Name):
|
|
254
|
+
return False
|
|
255
|
+
if if_stmt.test.id != condition_var:
|
|
256
|
+
return False
|
|
257
|
+
if len(if_stmt.body) != 1:
|
|
258
|
+
return False
|
|
259
|
+
return _is_append_call(if_stmt.body[0], result_var, appended_var)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _extract_assign_if_append(body: list[ast.stmt], result_var: str) -> tuple[str, str] | None:
|
|
263
|
+
"""Extract assign, if, append pattern from loop body.
|
|
264
|
+
|
|
265
|
+
Pattern: y = f(x); if y: result.append(y)
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
body: List of statements in for loop body
|
|
269
|
+
result_var: Name of the result list variable
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Tuple of (transform_var, transform_expr) if pattern matches, None otherwise
|
|
273
|
+
"""
|
|
274
|
+
if len(body) != 2:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
# First statement should be assignment
|
|
278
|
+
assignment = _extract_simple_assignment(body[0])
|
|
279
|
+
if assignment is None:
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
transform_var, transform_expr = assignment
|
|
283
|
+
|
|
284
|
+
# Second statement should be: if transform_var: result.append(transform_var)
|
|
285
|
+
if not isinstance(body[1], ast.If):
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
if not _is_conditional_append(body[1], transform_var, result_var, transform_var):
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
return (transform_var, transform_expr)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _is_simple_if_break(stmt: ast.stmt) -> ast.If | None:
|
|
295
|
+
"""Check if statement is simple 'if cond: break' with no else.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
stmt: Statement to check
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
The if statement if pattern matches, None otherwise
|
|
302
|
+
"""
|
|
303
|
+
if not isinstance(stmt, ast.If):
|
|
304
|
+
return None
|
|
305
|
+
if stmt.orelse:
|
|
306
|
+
return None
|
|
307
|
+
if len(stmt.body) != 1:
|
|
308
|
+
return None
|
|
309
|
+
if not isinstance(stmt.body[0], ast.Break):
|
|
310
|
+
return None
|
|
311
|
+
return stmt
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _extract_if_break_append(body: list[ast.stmt], result_var: str) -> ast.expr | None:
|
|
315
|
+
"""Extract if break, append pattern from loop body.
|
|
316
|
+
|
|
317
|
+
Pattern: if not cond: break; result.append(x)
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
body: List of statements in for loop body
|
|
321
|
+
result_var: Name of the result list variable
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
The condition (inverted for takewhile) if pattern matches, None otherwise
|
|
325
|
+
"""
|
|
326
|
+
if len(body) != 2:
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
if_stmt = _is_simple_if_break(body[0])
|
|
330
|
+
if if_stmt is None:
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
if not isinstance(body[1], ast.Expr):
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
return _invert_condition(if_stmt.test)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _get_method_call_info(stmt: ast.stmt) -> tuple[str, str, list[ast.expr]] | None:
|
|
340
|
+
"""Extract method call info from statement: obj.method(args).
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
stmt: Statement to check
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Tuple of (object_name, method_name, args) if method call, None otherwise
|
|
347
|
+
"""
|
|
348
|
+
if not isinstance(stmt, ast.Expr):
|
|
349
|
+
return None
|
|
350
|
+
if not isinstance(stmt.value, ast.Call):
|
|
351
|
+
return None
|
|
352
|
+
call = stmt.value
|
|
353
|
+
if not isinstance(call.func, ast.Attribute):
|
|
354
|
+
return None
|
|
355
|
+
if not isinstance(call.func.value, ast.Name):
|
|
356
|
+
return None
|
|
357
|
+
return (call.func.value.id, call.func.attr, call.args)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _is_single_name_arg(args: list[ast.expr], expected_name: str) -> bool:
|
|
361
|
+
"""Check if args list is single Name with expected id."""
|
|
362
|
+
if len(args) != 1:
|
|
363
|
+
return False
|
|
364
|
+
if not isinstance(args[0], ast.Name):
|
|
365
|
+
return False
|
|
366
|
+
return args[0].id == expected_name
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _is_append_call(stmt: ast.stmt, result_var: str, appended_var: str) -> bool:
|
|
370
|
+
"""Check if statement is result.append(appended_var)."""
|
|
371
|
+
info = _get_method_call_info(stmt)
|
|
372
|
+
if info is None:
|
|
373
|
+
return False
|
|
374
|
+
obj_name, method_name, args = info
|
|
375
|
+
if obj_name != result_var or method_name != "append":
|
|
376
|
+
return False
|
|
377
|
+
return _is_single_name_arg(args, appended_var)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _is_return_var_after(func_body: list[ast.stmt], for_index: int, var_name: str) -> bool:
|
|
381
|
+
"""Check if the next statement after for loop is return var_name."""
|
|
382
|
+
stmt = ast_utils.get_next_return_stmt(func_body, for_index)
|
|
383
|
+
if stmt is None:
|
|
384
|
+
return False
|
|
385
|
+
|
|
386
|
+
if stmt.value is None:
|
|
387
|
+
return False
|
|
388
|
+
if not isinstance(stmt.value, ast.Name):
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
return stmt.value.id == var_name
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _invert_condition(node: ast.expr) -> ast.expr:
|
|
395
|
+
"""Invert a boolean condition.
|
|
396
|
+
|
|
397
|
+
If condition is 'not x', returns 'x'.
|
|
398
|
+
Otherwise wraps in 'not (...)'.
|
|
399
|
+
"""
|
|
400
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
|
|
401
|
+
return node.operand
|
|
402
|
+
return ast.UnaryOp(op=ast.Not(), operand=node)
|