thailint 0.12.0__py3-none-any.whl → 0.13.0__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/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +3 -0
- src/cli/config.py +12 -12
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +3 -0
- src/cli/linters/code_patterns.py +113 -5
- src/cli/linters/code_smells.py +4 -0
- src/cli/linters/documentation.py +3 -0
- src/cli/linters/structure.py +3 -0
- src/cli/linters/structure_quality.py +3 -0
- src/cli_main.py +3 -0
- src/config.py +2 -1
- src/core/base.py +3 -2
- src/core/cli_utils.py +3 -1
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +4 -0
- src/core/rule_discovery.py +5 -1
- src/core/violation_builder.py +3 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +225 -383
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -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 +12 -0
- src/linters/collection_pipeline/continue_analyzer.py +2 -8
- src/linters/collection_pipeline/detector.py +262 -32
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +18 -35
- src/linters/collection_pipeline/suggestion_builder.py +68 -1
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +7 -4
- src/linters/dry/cache.py +7 -2
- src/linters/dry/config.py +7 -1
- src/linters/dry/constant_matcher.py +34 -25
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +48 -25
- src/linters/dry/python_analyzer.py +18 -10
- src/linters/dry/python_constant_extractor.py +51 -52
- src/linters/dry/single_statement_detector.py +14 -12
- src/linters/dry/token_hasher.py +115 -115
- src/linters/dry/typescript_analyzer.py +11 -6
- src/linters/dry/typescript_constant_extractor.py +4 -0
- src/linters/dry/typescript_statement_detector.py +208 -208
- src/linters/dry/typescript_value_extractor.py +3 -0
- src/linters/dry/violation_filter.py +1 -4
- src/linters/dry/violation_generator.py +1 -4
- src/linters/file_header/atemporal_detector.py +4 -0
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/field_validator.py +5 -8
- src/linters/file_header/linter.py +19 -12
- src/linters/file_header/markdown_parser.py +6 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/linter.py +22 -8
- src/linters/file_placement/pattern_matcher.py +21 -4
- src/linters/file_placement/pattern_validator.py +21 -7
- src/linters/file_placement/rule_checker.py +2 -2
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +66 -0
- src/linters/lazy_ignores/directive_utils.py +121 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +135 -0
- src/linters/lazy_ignores/python_analyzer.py +201 -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 +67 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +131 -0
- src/linters/lbyl/__init__.py +29 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/pattern_detectors/__init__.py +25 -0
- src/linters/lbyl/pattern_detectors/base.py +46 -0
- src/linters/magic_numbers/context_analyzer.py +227 -229
- src/linters/magic_numbers/linter.py +20 -15
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -16
- src/linters/method_property/config.py +4 -0
- src/linters/method_property/linter.py +5 -4
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- src/linters/print_statements/linter.py +6 -4
- src/linters/print_statements/python_analyzer.py +85 -81
- src/linters/print_statements/typescript_analyzer.py +6 -15
- src/linters/srp/heuristics.py +4 -4
- src/linters/srp/linter.py +12 -12
- src/linters/srp/violation_builder.py +0 -4
- src/linters/stateless_class/linter.py +30 -36
- src/linters/stateless_class/python_analyzer.py +11 -20
- src/linters/stringly_typed/config.py +4 -5
- src/linters/stringly_typed/context_filter.py +410 -410
- src/linters/stringly_typed/function_call_violation_builder.py +93 -95
- src/linters/stringly_typed/linter.py +48 -16
- src/linters/stringly_typed/python/analyzer.py +5 -1
- src/linters/stringly_typed/python/call_tracker.py +8 -5
- src/linters/stringly_typed/python/comparison_tracker.py +10 -5
- src/linters/stringly_typed/python/condition_extractor.py +3 -0
- src/linters/stringly_typed/python/conditional_detector.py +4 -1
- src/linters/stringly_typed/python/match_analyzer.py +8 -2
- src/linters/stringly_typed/python/validation_detector.py +3 -0
- src/linters/stringly_typed/storage.py +14 -14
- src/linters/stringly_typed/typescript/call_tracker.py +9 -3
- src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
- src/linters/stringly_typed/violation_generator.py +288 -259
- src/orchestrator/core.py +13 -4
- src/templates/thailint_config_template.yaml +166 -0
- src/utils/project_root.py +3 -0
- thailint-0.13.0.dist-info/METADATA +184 -0
- thailint-0.13.0.dist-info/RECORD +189 -0
- thailint-0.12.0.dist-info/METADATA +0 -1667
- thailint-0.12.0.dist-info/RECORD +0 -164
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,17 +12,40 @@ Overview: Implements the core detection logic for identifying imperative loop pa
|
|
|
12
12
|
|
|
13
13
|
Dependencies: ast module, continue_analyzer, suggestion_builder
|
|
14
14
|
|
|
15
|
-
Exports: PipelinePatternDetector class, PatternMatch dataclass
|
|
15
|
+
Exports: PipelinePatternDetector class, PatternMatch dataclass, PatternType enum
|
|
16
16
|
|
|
17
17
|
Interfaces: PipelinePatternDetector.detect_patterns() -> list[PatternMatch]
|
|
18
18
|
|
|
19
19
|
Implementation: AST visitor pattern with delegated pattern matching and suggestion generation
|
|
20
|
+
|
|
21
|
+
Suppressions:
|
|
22
|
+
- invalid-name: AST NodeVisitor visit_* methods follow convention, not PEP8
|
|
20
23
|
"""
|
|
21
24
|
|
|
22
25
|
import ast
|
|
23
|
-
from dataclasses import dataclass
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from enum import Enum
|
|
28
|
+
|
|
29
|
+
from . import any_all_analyzer, continue_analyzer, filter_map_analyzer, suggestion_builder
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PatternType(Enum):
|
|
33
|
+
"""Type of collection pipeline anti-pattern detected."""
|
|
34
|
+
|
|
35
|
+
EMBEDDED_FILTER = "embedded-filter"
|
|
36
|
+
"""for x: if not cond: continue; action(x) -> generator expression"""
|
|
24
37
|
|
|
25
|
-
|
|
38
|
+
ANY_PATTERN = "any-pattern"
|
|
39
|
+
"""for x: if cond: return True; return False -> any()"""
|
|
40
|
+
|
|
41
|
+
ALL_PATTERN = "all-pattern"
|
|
42
|
+
"""for x: if not cond: return False; return True -> all()"""
|
|
43
|
+
|
|
44
|
+
FILTER_MAP = "filter-map"
|
|
45
|
+
"""result=[]; for x: y=f(x); if y: result.append(y) -> list comprehension"""
|
|
46
|
+
|
|
47
|
+
TAKEWHILE = "takewhile"
|
|
48
|
+
"""result=[]; for x: if not cond: break; result.append(x) -> takewhile()"""
|
|
26
49
|
|
|
27
50
|
|
|
28
51
|
@dataclass
|
|
@@ -47,6 +70,161 @@ class PatternMatch:
|
|
|
47
70
|
suggestion: str
|
|
48
71
|
"""Refactoring suggestion as a code snippet."""
|
|
49
72
|
|
|
73
|
+
pattern_type: PatternType = field(default=PatternType.EMBEDDED_FILTER)
|
|
74
|
+
"""Type of anti-pattern detected (default: EMBEDDED_FILTER for backward compat)."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Module-level pattern match factory functions (extracted from class to reduce SRP violations)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def create_any_match(match: any_all_analyzer.AnyAllMatch) -> PatternMatch:
|
|
81
|
+
"""Create PatternMatch for any() pattern.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
match: AnyAllMatch from analyzer
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
PatternMatch for the any() pattern
|
|
88
|
+
"""
|
|
89
|
+
loop_var = suggestion_builder.get_target_name(match.for_node.target)
|
|
90
|
+
iterable = ast.unparse(match.for_node.iter)
|
|
91
|
+
condition = ast.unparse(match.condition)
|
|
92
|
+
suggestion = suggestion_builder.build_any_suggestion(loop_var, iterable, condition)
|
|
93
|
+
|
|
94
|
+
return PatternMatch(
|
|
95
|
+
line_number=match.for_node.lineno,
|
|
96
|
+
loop_var=loop_var,
|
|
97
|
+
iterable=iterable,
|
|
98
|
+
conditions=[condition],
|
|
99
|
+
has_side_effects=False,
|
|
100
|
+
suggestion=suggestion,
|
|
101
|
+
pattern_type=PatternType.ANY_PATTERN,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def create_all_match(match: any_all_analyzer.AnyAllMatch) -> PatternMatch:
|
|
106
|
+
"""Create PatternMatch for all() pattern.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
match: AnyAllMatch from analyzer
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
PatternMatch for the all() pattern
|
|
113
|
+
"""
|
|
114
|
+
loop_var = suggestion_builder.get_target_name(match.for_node.target)
|
|
115
|
+
iterable = ast.unparse(match.for_node.iter)
|
|
116
|
+
condition = ast.unparse(match.condition)
|
|
117
|
+
suggestion = suggestion_builder.build_all_suggestion(loop_var, iterable, condition)
|
|
118
|
+
|
|
119
|
+
return PatternMatch(
|
|
120
|
+
line_number=match.for_node.lineno,
|
|
121
|
+
loop_var=loop_var,
|
|
122
|
+
iterable=iterable,
|
|
123
|
+
conditions=[condition],
|
|
124
|
+
has_side_effects=False,
|
|
125
|
+
suggestion=suggestion,
|
|
126
|
+
pattern_type=PatternType.ALL_PATTERN,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def create_filter_map_match(match: filter_map_analyzer.FilterMapMatch) -> PatternMatch:
|
|
131
|
+
"""Create PatternMatch for filter-map pattern.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
match: FilterMapMatch from analyzer
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
PatternMatch for the filter-map pattern
|
|
138
|
+
"""
|
|
139
|
+
loop_var = suggestion_builder.get_target_name(match.for_node.target)
|
|
140
|
+
iterable = ast.unparse(match.for_node.iter)
|
|
141
|
+
suggestion = suggestion_builder.build_filter_map_suggestion(
|
|
142
|
+
loop_var, iterable, match.transform_var, match.transform_expr
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return PatternMatch(
|
|
146
|
+
line_number=match.for_node.lineno,
|
|
147
|
+
loop_var=loop_var,
|
|
148
|
+
iterable=iterable,
|
|
149
|
+
conditions=[match.transform_expr],
|
|
150
|
+
has_side_effects=False,
|
|
151
|
+
suggestion=suggestion,
|
|
152
|
+
pattern_type=PatternType.FILTER_MAP,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def create_takewhile_match(match: filter_map_analyzer.TakewhileMatch) -> PatternMatch:
|
|
157
|
+
"""Create PatternMatch for takewhile pattern.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
match: TakewhileMatch from analyzer
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
PatternMatch for the takewhile pattern
|
|
164
|
+
"""
|
|
165
|
+
loop_var = suggestion_builder.get_target_name(match.for_node.target)
|
|
166
|
+
iterable = ast.unparse(match.for_node.iter)
|
|
167
|
+
condition = ast.unparse(match.condition)
|
|
168
|
+
suggestion = suggestion_builder.build_takewhile_suggestion(loop_var, iterable, condition)
|
|
169
|
+
|
|
170
|
+
return PatternMatch(
|
|
171
|
+
line_number=match.for_node.lineno,
|
|
172
|
+
loop_var=loop_var,
|
|
173
|
+
iterable=iterable,
|
|
174
|
+
conditions=[condition],
|
|
175
|
+
has_side_effects=False,
|
|
176
|
+
suggestion=suggestion,
|
|
177
|
+
pattern_type=PatternType.TAKEWHILE,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def create_embedded_filter_match(for_node: ast.For, continues: list[ast.If]) -> PatternMatch:
|
|
182
|
+
"""Create a PatternMatch for embedded filter pattern.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
for_node: AST For node
|
|
186
|
+
continues: List of continue guard if statements
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
PatternMatch object with detection information
|
|
190
|
+
"""
|
|
191
|
+
loop_var = suggestion_builder.get_target_name(for_node.target)
|
|
192
|
+
iterable = ast.unparse(for_node.iter)
|
|
193
|
+
conditions = [suggestion_builder.invert_condition(c.test) for c in continues]
|
|
194
|
+
suggestion = suggestion_builder.build_suggestion(loop_var, iterable, conditions)
|
|
195
|
+
|
|
196
|
+
return PatternMatch(
|
|
197
|
+
line_number=for_node.lineno,
|
|
198
|
+
loop_var=loop_var,
|
|
199
|
+
iterable=iterable,
|
|
200
|
+
conditions=conditions,
|
|
201
|
+
has_side_effects=False,
|
|
202
|
+
suggestion=suggestion,
|
|
203
|
+
pattern_type=PatternType.EMBEDDED_FILTER,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def analyze_for_loop(node: ast.For) -> PatternMatch | None:
|
|
208
|
+
"""Analyze a for loop for embedded filtering patterns.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
node: AST For node to analyze
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
PatternMatch if pattern detected, None otherwise
|
|
215
|
+
"""
|
|
216
|
+
continues = continue_analyzer.extract_continue_patterns(node.body)
|
|
217
|
+
if not continues:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
if continue_analyzer.has_side_effects(continues):
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
if not continue_analyzer.has_body_after_continues(node.body, len(continues)):
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
return create_embedded_filter_match(node, continues)
|
|
227
|
+
|
|
50
228
|
|
|
51
229
|
class PipelinePatternDetector(ast.NodeVisitor):
|
|
52
230
|
"""Detects for loops with embedded filtering via if/continue patterns."""
|
|
@@ -59,6 +237,7 @@ class PipelinePatternDetector(ast.NodeVisitor):
|
|
|
59
237
|
"""
|
|
60
238
|
self.source_code = source_code
|
|
61
239
|
self.matches: list[PatternMatch] = []
|
|
240
|
+
self._func_body_stack: list[list[ast.stmt]] = []
|
|
62
241
|
|
|
63
242
|
def detect_patterns(self) -> list[PatternMatch]:
|
|
64
243
|
"""Analyze source code and return detected patterns.
|
|
@@ -73,58 +252,109 @@ class PipelinePatternDetector(ast.NodeVisitor):
|
|
|
73
252
|
pass # Invalid Python, return empty list
|
|
74
253
|
return self.matches
|
|
75
254
|
|
|
255
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # pylint: disable=invalid-name
|
|
256
|
+
"""Visit function and track body for any/all pattern detection.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
node: AST FunctionDef node
|
|
260
|
+
"""
|
|
261
|
+
self._func_body_stack.append(node.body)
|
|
262
|
+
self.generic_visit(node)
|
|
263
|
+
self._func_body_stack.pop()
|
|
264
|
+
|
|
265
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: # pylint: disable=invalid-name
|
|
266
|
+
"""Visit async function and track body for any/all pattern detection.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
node: AST AsyncFunctionDef node
|
|
270
|
+
"""
|
|
271
|
+
self._func_body_stack.append(node.body)
|
|
272
|
+
self.generic_visit(node)
|
|
273
|
+
self._func_body_stack.pop()
|
|
274
|
+
|
|
76
275
|
def visit_For(self, node: ast.For) -> None: # pylint: disable=invalid-name
|
|
77
276
|
"""Visit for loop and check for filtering patterns.
|
|
78
277
|
|
|
79
278
|
Args:
|
|
80
279
|
node: AST For node to analyze
|
|
81
280
|
"""
|
|
82
|
-
match = self.
|
|
281
|
+
match = self._find_pattern_match(node)
|
|
83
282
|
if match is not None:
|
|
84
283
|
self.matches.append(match)
|
|
85
284
|
self.generic_visit(node)
|
|
86
285
|
|
|
87
|
-
def
|
|
88
|
-
"""
|
|
286
|
+
def _find_pattern_match(self, node: ast.For) -> PatternMatch | None:
|
|
287
|
+
"""Find the first matching anti-pattern for a for loop.
|
|
288
|
+
|
|
289
|
+
Checks patterns in priority order: any/all, filter-map/takewhile, embedded filter.
|
|
89
290
|
|
|
90
291
|
Args:
|
|
91
292
|
node: AST For node to analyze
|
|
92
293
|
|
|
93
294
|
Returns:
|
|
94
|
-
PatternMatch if pattern detected, None otherwise
|
|
295
|
+
PatternMatch if any pattern detected, None otherwise
|
|
95
296
|
"""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
297
|
+
# Check for any/all patterns (requires function context)
|
|
298
|
+
any_all_match = self._analyze_any_all_pattern(node)
|
|
299
|
+
if any_all_match is not None:
|
|
300
|
+
return any_all_match
|
|
99
301
|
|
|
100
|
-
|
|
101
|
-
|
|
302
|
+
# Check for filter-map/takewhile patterns
|
|
303
|
+
filter_map_match = self._analyze_filter_map_pattern(node)
|
|
304
|
+
if filter_map_match is not None:
|
|
305
|
+
return filter_map_match
|
|
306
|
+
|
|
307
|
+
# Check for embedded filter patterns
|
|
308
|
+
return analyze_for_loop(node)
|
|
309
|
+
|
|
310
|
+
def _analyze_any_all_pattern(self, node: ast.For) -> PatternMatch | None:
|
|
311
|
+
"""Analyze a for loop for any()/all() patterns.
|
|
102
312
|
|
|
103
|
-
|
|
313
|
+
Args:
|
|
314
|
+
node: AST For node to analyze
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
PatternMatch if any/all pattern detected, None otherwise
|
|
318
|
+
"""
|
|
319
|
+
if not self._func_body_stack:
|
|
104
320
|
return None
|
|
105
321
|
|
|
106
|
-
|
|
322
|
+
func_body = self._func_body_stack[-1]
|
|
107
323
|
|
|
108
|
-
|
|
109
|
-
|
|
324
|
+
# Try any() pattern first
|
|
325
|
+
any_match = any_all_analyzer.extract_any_pattern(func_body, node)
|
|
326
|
+
if any_match is not None:
|
|
327
|
+
return create_any_match(any_match)
|
|
328
|
+
|
|
329
|
+
# Try all() pattern
|
|
330
|
+
all_match = any_all_analyzer.extract_all_pattern(func_body, node)
|
|
331
|
+
if all_match is not None:
|
|
332
|
+
return create_all_match(all_match)
|
|
333
|
+
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
def _analyze_filter_map_pattern(self, node: ast.For) -> PatternMatch | None:
|
|
337
|
+
"""Analyze a for loop for filter-map/takewhile patterns.
|
|
110
338
|
|
|
111
339
|
Args:
|
|
112
|
-
|
|
113
|
-
continues: List of continue guard if statements
|
|
340
|
+
node: AST For node to analyze
|
|
114
341
|
|
|
115
342
|
Returns:
|
|
116
|
-
PatternMatch
|
|
343
|
+
PatternMatch if filter-map/takewhile pattern detected, None otherwise
|
|
117
344
|
"""
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
345
|
+
if not self._func_body_stack:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
func_body = self._func_body_stack[-1]
|
|
349
|
+
|
|
350
|
+
# Try filter-map pattern first
|
|
351
|
+
fm_match = filter_map_analyzer.extract_filter_map_pattern(func_body, node)
|
|
352
|
+
if fm_match is not None:
|
|
353
|
+
return create_filter_map_match(fm_match)
|
|
354
|
+
|
|
355
|
+
# Try takewhile pattern
|
|
356
|
+
tw_match = filter_map_analyzer.extract_takewhile_pattern(func_body, node)
|
|
357
|
+
if tw_match is not None:
|
|
358
|
+
return create_takewhile_match(tw_match)
|
|
359
|
+
|
|
360
|
+
return None
|