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.
Files changed (121) hide show
  1. src/analyzers/__init__.py +4 -3
  2. src/analyzers/ast_utils.py +54 -0
  3. src/analyzers/typescript_base.py +4 -0
  4. src/cli/__init__.py +3 -0
  5. src/cli/config.py +12 -12
  6. src/cli/config_merge.py +241 -0
  7. src/cli/linters/__init__.py +3 -0
  8. src/cli/linters/code_patterns.py +113 -5
  9. src/cli/linters/code_smells.py +4 -0
  10. src/cli/linters/documentation.py +3 -0
  11. src/cli/linters/structure.py +3 -0
  12. src/cli/linters/structure_quality.py +3 -0
  13. src/cli_main.py +3 -0
  14. src/config.py +2 -1
  15. src/core/base.py +3 -2
  16. src/core/cli_utils.py +3 -1
  17. src/core/config_parser.py +5 -2
  18. src/core/constants.py +54 -0
  19. src/core/linter_utils.py +4 -0
  20. src/core/rule_discovery.py +5 -1
  21. src/core/violation_builder.py +3 -0
  22. src/linter_config/directive_markers.py +109 -0
  23. src/linter_config/ignore.py +225 -383
  24. src/linter_config/pattern_utils.py +65 -0
  25. src/linter_config/rule_matcher.py +89 -0
  26. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  27. src/linters/collection_pipeline/ast_utils.py +40 -0
  28. src/linters/collection_pipeline/config.py +12 -0
  29. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  30. src/linters/collection_pipeline/detector.py +262 -32
  31. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  32. src/linters/collection_pipeline/linter.py +18 -35
  33. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  34. src/linters/dry/base_token_analyzer.py +16 -9
  35. src/linters/dry/block_filter.py +7 -4
  36. src/linters/dry/cache.py +7 -2
  37. src/linters/dry/config.py +7 -1
  38. src/linters/dry/constant_matcher.py +34 -25
  39. src/linters/dry/file_analyzer.py +4 -2
  40. src/linters/dry/inline_ignore.py +7 -16
  41. src/linters/dry/linter.py +48 -25
  42. src/linters/dry/python_analyzer.py +18 -10
  43. src/linters/dry/python_constant_extractor.py +51 -52
  44. src/linters/dry/single_statement_detector.py +14 -12
  45. src/linters/dry/token_hasher.py +115 -115
  46. src/linters/dry/typescript_analyzer.py +11 -6
  47. src/linters/dry/typescript_constant_extractor.py +4 -0
  48. src/linters/dry/typescript_statement_detector.py +208 -208
  49. src/linters/dry/typescript_value_extractor.py +3 -0
  50. src/linters/dry/violation_filter.py +1 -4
  51. src/linters/dry/violation_generator.py +1 -4
  52. src/linters/file_header/atemporal_detector.py +4 -0
  53. src/linters/file_header/base_parser.py +4 -0
  54. src/linters/file_header/bash_parser.py +4 -0
  55. src/linters/file_header/field_validator.py +5 -8
  56. src/linters/file_header/linter.py +19 -12
  57. src/linters/file_header/markdown_parser.py +6 -0
  58. src/linters/file_placement/config_loader.py +3 -1
  59. src/linters/file_placement/linter.py +22 -8
  60. src/linters/file_placement/pattern_matcher.py +21 -4
  61. src/linters/file_placement/pattern_validator.py +21 -7
  62. src/linters/file_placement/rule_checker.py +2 -2
  63. src/linters/lazy_ignores/__init__.py +43 -0
  64. src/linters/lazy_ignores/config.py +66 -0
  65. src/linters/lazy_ignores/directive_utils.py +121 -0
  66. src/linters/lazy_ignores/header_parser.py +177 -0
  67. src/linters/lazy_ignores/linter.py +158 -0
  68. src/linters/lazy_ignores/matcher.py +135 -0
  69. src/linters/lazy_ignores/python_analyzer.py +201 -0
  70. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  71. src/linters/lazy_ignores/skip_detector.py +298 -0
  72. src/linters/lazy_ignores/types.py +67 -0
  73. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  74. src/linters/lazy_ignores/violation_builder.py +131 -0
  75. src/linters/lbyl/__init__.py +29 -0
  76. src/linters/lbyl/config.py +63 -0
  77. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  78. src/linters/lbyl/pattern_detectors/base.py +46 -0
  79. src/linters/magic_numbers/context_analyzer.py +227 -229
  80. src/linters/magic_numbers/linter.py +20 -15
  81. src/linters/magic_numbers/python_analyzer.py +4 -16
  82. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  83. src/linters/method_property/config.py +4 -0
  84. src/linters/method_property/linter.py +5 -4
  85. src/linters/method_property/python_analyzer.py +5 -4
  86. src/linters/method_property/violation_builder.py +3 -0
  87. src/linters/nesting/typescript_analyzer.py +6 -12
  88. src/linters/nesting/typescript_function_extractor.py +0 -4
  89. src/linters/print_statements/linter.py +6 -4
  90. src/linters/print_statements/python_analyzer.py +85 -81
  91. src/linters/print_statements/typescript_analyzer.py +6 -15
  92. src/linters/srp/heuristics.py +4 -4
  93. src/linters/srp/linter.py +12 -12
  94. src/linters/srp/violation_builder.py +0 -4
  95. src/linters/stateless_class/linter.py +30 -36
  96. src/linters/stateless_class/python_analyzer.py +11 -20
  97. src/linters/stringly_typed/config.py +4 -5
  98. src/linters/stringly_typed/context_filter.py +410 -410
  99. src/linters/stringly_typed/function_call_violation_builder.py +93 -95
  100. src/linters/stringly_typed/linter.py +48 -16
  101. src/linters/stringly_typed/python/analyzer.py +5 -1
  102. src/linters/stringly_typed/python/call_tracker.py +8 -5
  103. src/linters/stringly_typed/python/comparison_tracker.py +10 -5
  104. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  105. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  106. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  107. src/linters/stringly_typed/python/validation_detector.py +3 -0
  108. src/linters/stringly_typed/storage.py +14 -14
  109. src/linters/stringly_typed/typescript/call_tracker.py +9 -3
  110. src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
  111. src/linters/stringly_typed/violation_generator.py +288 -259
  112. src/orchestrator/core.py +13 -4
  113. src/templates/thailint_config_template.yaml +166 -0
  114. src/utils/project_root.py +3 -0
  115. thailint-0.13.0.dist-info/METADATA +184 -0
  116. thailint-0.13.0.dist-info/RECORD +189 -0
  117. thailint-0.12.0.dist-info/METADATA +0 -1667
  118. thailint-0.12.0.dist-info/RECORD +0 -164
  119. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
  120. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
  121. {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
- from . import continue_analyzer, suggestion_builder
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._analyze_for_loop(node)
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 _analyze_for_loop(self, node: ast.For) -> PatternMatch | None:
88
- """Analyze a for loop for embedded filtering patterns.
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
- continues = continue_analyzer.extract_continue_patterns(node.body)
97
- if not continues:
98
- return None
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
- if continue_analyzer.has_side_effects(continues):
101
- return None
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
- if not continue_analyzer.has_body_after_continues(node.body, len(continues)):
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
- return self._create_match(node, continues)
322
+ func_body = self._func_body_stack[-1]
107
323
 
108
- def _create_match(self, for_node: ast.For, continues: list[ast.If]) -> PatternMatch:
109
- """Create a PatternMatch from detected pattern.
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
- for_node: AST For node
113
- continues: List of continue guard if statements
340
+ node: AST For node to analyze
114
341
 
115
342
  Returns:
116
- PatternMatch object with detection information
343
+ PatternMatch if filter-map/takewhile pattern detected, None otherwise
117
344
  """
118
- loop_var = suggestion_builder.get_target_name(for_node.target)
119
- iterable = ast.unparse(for_node.iter)
120
- conditions = [suggestion_builder.invert_condition(c.test) for c in continues]
121
- suggestion = suggestion_builder.build_suggestion(loop_var, iterable, conditions)
122
-
123
- return PatternMatch(
124
- line_number=for_node.lineno,
125
- loop_var=loop_var,
126
- iterable=iterable,
127
- conditions=conditions,
128
- has_side_effects=False,
129
- suggestion=suggestion,
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