thailint 0.9.0__py3-none-any.whl → 0.11.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 (69) hide show
  1. src/__init__.py +1 -0
  2. src/cli/__init__.py +27 -0
  3. src/cli/__main__.py +22 -0
  4. src/cli/config.py +478 -0
  5. src/cli/linters/__init__.py +58 -0
  6. src/cli/linters/code_patterns.py +372 -0
  7. src/cli/linters/code_smells.py +343 -0
  8. src/cli/linters/documentation.py +155 -0
  9. src/cli/linters/shared.py +89 -0
  10. src/cli/linters/structure.py +313 -0
  11. src/cli/linters/structure_quality.py +316 -0
  12. src/cli/main.py +120 -0
  13. src/cli/utils.py +375 -0
  14. src/cli_main.py +34 -0
  15. src/config.py +2 -3
  16. src/core/rule_discovery.py +43 -10
  17. src/core/types.py +13 -0
  18. src/core/violation_utils.py +69 -0
  19. src/linter_config/ignore.py +32 -16
  20. src/linters/collection_pipeline/__init__.py +90 -0
  21. src/linters/collection_pipeline/config.py +63 -0
  22. src/linters/collection_pipeline/continue_analyzer.py +100 -0
  23. src/linters/collection_pipeline/detector.py +130 -0
  24. src/linters/collection_pipeline/linter.py +437 -0
  25. src/linters/collection_pipeline/suggestion_builder.py +63 -0
  26. src/linters/dry/block_filter.py +99 -9
  27. src/linters/dry/cache.py +94 -6
  28. src/linters/dry/config.py +47 -10
  29. src/linters/dry/constant.py +92 -0
  30. src/linters/dry/constant_matcher.py +214 -0
  31. src/linters/dry/constant_violation_builder.py +98 -0
  32. src/linters/dry/linter.py +89 -48
  33. src/linters/dry/python_analyzer.py +44 -431
  34. src/linters/dry/python_constant_extractor.py +101 -0
  35. src/linters/dry/single_statement_detector.py +415 -0
  36. src/linters/dry/token_hasher.py +5 -5
  37. src/linters/dry/typescript_analyzer.py +63 -382
  38. src/linters/dry/typescript_constant_extractor.py +134 -0
  39. src/linters/dry/typescript_statement_detector.py +255 -0
  40. src/linters/dry/typescript_value_extractor.py +66 -0
  41. src/linters/file_header/linter.py +9 -13
  42. src/linters/file_placement/linter.py +30 -10
  43. src/linters/file_placement/pattern_matcher.py +19 -5
  44. src/linters/magic_numbers/linter.py +8 -67
  45. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  46. src/linters/nesting/linter.py +12 -9
  47. src/linters/print_statements/linter.py +7 -24
  48. src/linters/srp/class_analyzer.py +9 -9
  49. src/linters/srp/heuristics.py +6 -5
  50. src/linters/srp/linter.py +4 -5
  51. src/linters/stateless_class/linter.py +2 -2
  52. src/linters/stringly_typed/__init__.py +23 -0
  53. src/linters/stringly_typed/config.py +165 -0
  54. src/linters/stringly_typed/python/__init__.py +29 -0
  55. src/linters/stringly_typed/python/analyzer.py +198 -0
  56. src/linters/stringly_typed/python/condition_extractor.py +131 -0
  57. src/linters/stringly_typed/python/conditional_detector.py +176 -0
  58. src/linters/stringly_typed/python/constants.py +21 -0
  59. src/linters/stringly_typed/python/match_analyzer.py +88 -0
  60. src/linters/stringly_typed/python/validation_detector.py +186 -0
  61. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  62. src/orchestrator/core.py +241 -12
  63. {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/METADATA +116 -3
  64. {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/RECORD +67 -29
  65. thailint-0.11.0.dist-info/entry_points.txt +4 -0
  66. src/cli.py +0 -2014
  67. thailint-0.9.0.dist-info/entry_points.txt +0 -4
  68. {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/WHEEL +0 -0
  69. {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,437 @@
1
+ """
2
+ Purpose: CollectionPipelineRule implementation for detecting loop filtering anti-patterns
3
+
4
+ Scope: Main rule class implementing BaseLintRule interface for collection-pipeline detection
5
+
6
+ Overview: Implements the BaseLintRule interface to detect for loops with embedded
7
+ filtering logic that could be refactored to collection pipelines. Detects patterns
8
+ like 'for x in iter: if not cond: continue; action(x)' which can be refactored to
9
+ use generator expressions or filter(). Based on Martin Fowler's refactoring pattern.
10
+ Integrates with thai-lint CLI and supports text, JSON, and SARIF output formats.
11
+ Supports comprehensive 5-level ignore system including project-level patterns,
12
+ linter-specific ignore patterns, file-level directives, line-level directives,
13
+ and block-level directives via IgnoreDirectiveParser.
14
+
15
+ Dependencies: BaseLintRule, BaseLintContext, Violation, PipelinePatternDetector,
16
+ CollectionPipelineConfig, IgnoreDirectiveParser
17
+
18
+ Exports: CollectionPipelineRule class
19
+
20
+ Interfaces: CollectionPipelineRule.check(context) -> list[Violation], rule metadata properties
21
+
22
+ Implementation: Uses PipelinePatternDetector for AST analysis, composition pattern with
23
+ config loading and comprehensive ignore checking via IgnoreDirectiveParser
24
+ """
25
+
26
+ from pathlib import Path
27
+
28
+ from src.core.base import BaseLintContext, BaseLintRule
29
+ from src.core.types import Severity, Violation
30
+ from src.linter_config.ignore import get_ignore_parser
31
+
32
+ from .config import CollectionPipelineConfig
33
+ from .detector import PatternMatch, PipelinePatternDetector
34
+
35
+
36
+ class CollectionPipelineRule(BaseLintRule): # thailint: ignore[srp,dry]
37
+ """Detects for loops with embedded filtering that could use collection pipelines."""
38
+
39
+ def __init__(self) -> None:
40
+ """Initialize the rule with ignore parser."""
41
+ self._ignore_parser = get_ignore_parser()
42
+
43
+ @property
44
+ def rule_id(self) -> str:
45
+ """Unique identifier for this rule."""
46
+ return "collection-pipeline.embedded-filter"
47
+
48
+ @property
49
+ def rule_name(self) -> str:
50
+ """Human-readable name for this rule."""
51
+ return "Embedded Loop Filtering"
52
+
53
+ @property
54
+ def description(self) -> str:
55
+ """Description of what this rule checks."""
56
+ return (
57
+ "For loops with embedded if/continue filtering patterns should be "
58
+ "refactored to use collection pipelines (generator expressions, filter())"
59
+ )
60
+
61
+ def check(self, context: BaseLintContext) -> list[Violation]:
62
+ """Check for collection pipeline anti-patterns.
63
+
64
+ Args:
65
+ context: Lint context with file information
66
+
67
+ Returns:
68
+ List of violations found
69
+ """
70
+ if not self._should_analyze(context):
71
+ return []
72
+
73
+ config = self._load_config(context)
74
+ if not config.enabled:
75
+ return []
76
+
77
+ if self._is_file_ignored(context, config):
78
+ return []
79
+
80
+ if self._has_file_level_ignore(context):
81
+ return []
82
+
83
+ return self._analyze_python(context, config)
84
+
85
+ def _should_analyze(self, context: BaseLintContext) -> bool:
86
+ """Check if context should be analyzed.
87
+
88
+ Args:
89
+ context: Lint context
90
+
91
+ Returns:
92
+ True if should analyze
93
+ """
94
+ return context.language == "python" and context.file_content is not None
95
+
96
+ def _get_config_dict(self, context: BaseLintContext) -> dict | None:
97
+ """Get configuration dictionary from context.
98
+
99
+ Args:
100
+ context: Lint context
101
+
102
+ Returns:
103
+ Config dict or None
104
+ """
105
+ if hasattr(context, "config") and context.config is not None:
106
+ return context.config
107
+ if hasattr(context, "metadata") and context.metadata is not None:
108
+ return context.metadata
109
+ return None
110
+
111
+ def _load_config(self, context: BaseLintContext) -> CollectionPipelineConfig:
112
+ """Load configuration from context.
113
+
114
+ Args:
115
+ context: Lint context
116
+
117
+ Returns:
118
+ CollectionPipelineConfig instance
119
+ """
120
+ config_dict = self._get_config_dict(context)
121
+ if config_dict is None or not isinstance(config_dict, dict):
122
+ return CollectionPipelineConfig()
123
+
124
+ # Check for collection_pipeline or collection-pipeline specific config
125
+ linter_config = config_dict.get(
126
+ "collection_pipeline", config_dict.get("collection-pipeline", config_dict)
127
+ )
128
+ return CollectionPipelineConfig.from_dict(linter_config)
129
+
130
+ def _is_file_ignored(self, context: BaseLintContext, config: CollectionPipelineConfig) -> bool:
131
+ """Check if file matches ignore patterns.
132
+
133
+ Args:
134
+ context: Lint context
135
+ config: Configuration
136
+
137
+ Returns:
138
+ True if file should be ignored
139
+ """
140
+ if not config.ignore:
141
+ return False
142
+
143
+ if not context.file_path:
144
+ return False
145
+
146
+ file_path = Path(context.file_path)
147
+ for pattern in config.ignore:
148
+ if self._matches_pattern(file_path, pattern):
149
+ return True
150
+ return False
151
+
152
+ def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
153
+ """Check if file path matches a glob pattern.
154
+
155
+ Args:
156
+ file_path: Path to check
157
+ pattern: Glob pattern
158
+
159
+ Returns:
160
+ True if path matches pattern
161
+ """
162
+ if file_path.match(pattern):
163
+ return True
164
+ if pattern in str(file_path):
165
+ return True
166
+ return False
167
+
168
+ def _has_file_level_ignore(self, context: BaseLintContext) -> bool:
169
+ """Check if file has file-level ignore directive.
170
+
171
+ Args:
172
+ context: Lint context
173
+
174
+ Returns:
175
+ True if file should be ignored at file level
176
+ """
177
+ if not context.file_content:
178
+ return False
179
+
180
+ # Check first 10 lines for ignore-file directive
181
+ lines = context.file_content.splitlines()[:10]
182
+ for line in lines:
183
+ if self._is_file_ignore_directive(line):
184
+ return True
185
+ return False
186
+
187
+ def _is_file_ignore_directive(self, line: str) -> bool:
188
+ """Check if line is a file-level ignore directive.
189
+
190
+ Args:
191
+ line: Line to check
192
+
193
+ Returns:
194
+ True if line has file-level ignore for this rule
195
+ """
196
+ line_lower = line.lower()
197
+ if "thailint: ignore-file" not in line_lower:
198
+ return False
199
+
200
+ # Check for general ignore-file (no rule specified)
201
+ if "ignore-file[" not in line_lower:
202
+ return True
203
+
204
+ # Check for rule-specific ignore
205
+ return self._matches_rule_ignore(line_lower, "ignore-file")
206
+
207
+ def _matches_rule_ignore(self, line: str, directive: str) -> bool:
208
+ """Check if line matches rule-specific ignore.
209
+
210
+ Args:
211
+ line: Line to check (lowercase)
212
+ directive: Directive name (ignore-file or ignore)
213
+
214
+ Returns:
215
+ True if ignore applies to this rule
216
+ """
217
+ import re
218
+
219
+ pattern = rf"{directive}\[([^\]]+)\]"
220
+ match = re.search(pattern, line)
221
+ if not match:
222
+ return False
223
+
224
+ rules = [r.strip().lower() for r in match.group(1).split(",")]
225
+ return any(self._rule_matches(r) for r in rules)
226
+
227
+ def _rule_matches(self, rule_pattern: str) -> bool:
228
+ """Check if rule pattern matches this rule.
229
+
230
+ Args:
231
+ rule_pattern: Rule pattern to check
232
+
233
+ Returns:
234
+ True if pattern matches this rule
235
+ """
236
+ rule_id_lower = self.rule_id.lower()
237
+ pattern_lower = rule_pattern.lower()
238
+
239
+ # Exact match
240
+ if rule_id_lower == pattern_lower:
241
+ return True
242
+
243
+ # Prefix match: collection-pipeline matches collection-pipeline.embedded-filter
244
+ if rule_id_lower.startswith(pattern_lower + "."):
245
+ return True
246
+
247
+ # Wildcard match: collection-pipeline.* matches collection-pipeline.embedded-filter
248
+ if pattern_lower.endswith("*"):
249
+ prefix = pattern_lower[:-1]
250
+ return rule_id_lower.startswith(prefix)
251
+
252
+ return False
253
+
254
+ def _analyze_python(
255
+ self, context: BaseLintContext, config: CollectionPipelineConfig
256
+ ) -> list[Violation]:
257
+ """Analyze Python code for collection pipeline patterns.
258
+
259
+ Args:
260
+ context: Lint context with Python file information
261
+ config: Collection pipeline configuration
262
+
263
+ Returns:
264
+ List of violations found
265
+ """
266
+ detector = PipelinePatternDetector(context.file_content or "")
267
+ matches = detector.detect_patterns()
268
+
269
+ return self._filter_matches_to_violations(matches, config, context)
270
+
271
+ def _filter_matches_to_violations(
272
+ self,
273
+ matches: list[PatternMatch],
274
+ config: CollectionPipelineConfig,
275
+ context: BaseLintContext,
276
+ ) -> list[Violation]:
277
+ """Filter matches by threshold and ignore rules.
278
+
279
+ Args:
280
+ matches: Detected pattern matches
281
+ config: Configuration with thresholds
282
+ context: Lint context
283
+
284
+ Returns:
285
+ List of violations after filtering
286
+ """
287
+ violations: list[Violation] = []
288
+ for match in matches:
289
+ violation = self._process_match(match, config, context)
290
+ if violation:
291
+ violations.append(violation)
292
+ return violations
293
+
294
+ def _process_match(
295
+ self,
296
+ match: PatternMatch,
297
+ config: CollectionPipelineConfig,
298
+ context: BaseLintContext,
299
+ ) -> Violation | None:
300
+ """Process a single match into a violation if applicable.
301
+
302
+ Args:
303
+ match: Pattern match to process
304
+ config: Configuration with thresholds
305
+ context: Lint context
306
+
307
+ Returns:
308
+ Violation if match should be reported, None otherwise
309
+ """
310
+ if len(match.conditions) < config.min_continues:
311
+ return None
312
+
313
+ violation = self._create_violation(match, context)
314
+ if self._should_ignore_violation(violation, match.line_number, context):
315
+ return None
316
+
317
+ return violation
318
+
319
+ def _should_ignore_violation(
320
+ self, violation: Violation, line_num: int, context: BaseLintContext
321
+ ) -> bool:
322
+ """Check if violation should be ignored.
323
+
324
+ Args:
325
+ violation: Violation to check
326
+ line_num: Line number of the violation
327
+ context: Lint context
328
+
329
+ Returns:
330
+ True if violation should be ignored
331
+ """
332
+ if not context.file_content:
333
+ return False
334
+
335
+ # Check using IgnoreDirectiveParser for comprehensive ignore checking
336
+ if self._ignore_parser.should_ignore_violation(violation, context.file_content):
337
+ return True
338
+
339
+ # Also check inline ignore on loop line
340
+ return self._has_inline_ignore(line_num, context)
341
+
342
+ def _has_inline_ignore(self, line_num: int, context: BaseLintContext) -> bool:
343
+ """Check for inline ignore directive on loop line.
344
+
345
+ Args:
346
+ line_num: Line number to check
347
+ context: Lint context
348
+
349
+ Returns:
350
+ True if line has ignore directive
351
+ """
352
+ line = self._get_line_text(line_num, context)
353
+ if not line:
354
+ return False
355
+
356
+ return self._is_ignore_directive(line.lower())
357
+
358
+ def _get_line_text(self, line_num: int, context: BaseLintContext) -> str | None:
359
+ """Get text of a specific line.
360
+
361
+ Args:
362
+ line_num: Line number (1-indexed)
363
+ context: Lint context
364
+
365
+ Returns:
366
+ Line text or None if invalid
367
+ """
368
+ if not context.file_content:
369
+ return None
370
+
371
+ lines = context.file_content.splitlines()
372
+ if line_num <= 0 or line_num > len(lines):
373
+ return None
374
+
375
+ return lines[line_num - 1]
376
+
377
+ def _is_ignore_directive(self, line: str) -> bool:
378
+ """Check if line contains ignore directive for this rule.
379
+
380
+ Args:
381
+ line: Line text (lowercase)
382
+
383
+ Returns:
384
+ True if line has applicable ignore directive
385
+ """
386
+ if "thailint:" not in line or "ignore" not in line:
387
+ return False
388
+
389
+ # General ignore (no rule specified)
390
+ if "ignore[" not in line:
391
+ return True
392
+
393
+ # Rule-specific ignore
394
+ return self._matches_rule_ignore(line, "ignore")
395
+
396
+ def _create_violation(self, match: PatternMatch, context: BaseLintContext) -> Violation:
397
+ """Create a Violation from a PatternMatch.
398
+
399
+ Args:
400
+ match: Detected pattern match
401
+ context: Lint context
402
+
403
+ Returns:
404
+ Violation object for the detected pattern
405
+ """
406
+ message = self._build_message(match)
407
+ file_path = str(context.file_path) if context.file_path else "unknown"
408
+
409
+ return Violation(
410
+ rule_id=self.rule_id,
411
+ file_path=file_path,
412
+ line=match.line_number,
413
+ column=0,
414
+ message=message,
415
+ severity=Severity.ERROR,
416
+ suggestion=match.suggestion,
417
+ )
418
+
419
+ def _build_message(self, match: PatternMatch) -> str:
420
+ """Build violation message.
421
+
422
+ Args:
423
+ match: Detected pattern match
424
+
425
+ Returns:
426
+ Human-readable message describing the violation
427
+ """
428
+ num_conditions = len(match.conditions)
429
+ if num_conditions == 1:
430
+ return (
431
+ f"For loop over '{match.iterable}' has embedded filtering. "
432
+ f"Consider using a generator expression."
433
+ )
434
+ return (
435
+ f"For loop over '{match.iterable}' has {num_conditions} filter conditions. "
436
+ f"Consider combining into a collection pipeline."
437
+ )
@@ -0,0 +1,63 @@
1
+ """
2
+ Purpose: Build refactoring suggestions for collection pipeline patterns
3
+
4
+ Scope: Generate code suggestions for converting embedded filtering to collection pipelines
5
+
6
+ Overview: Provides helper functions for generating refactoring suggestions when embedded
7
+ filtering patterns are detected. Handles condition inversion (converting continue guard
8
+ conditions to filter conditions), target name extraction, and suggestion string generation.
9
+ Separates suggestion logic from pattern detection logic for better maintainability.
10
+
11
+ Dependencies: ast module for Python AST processing
12
+
13
+ Exports: build_suggestion, invert_condition, get_target_name
14
+
15
+ Interfaces: Functions for suggestion generation and condition transformation
16
+
17
+ Implementation: AST-based condition inversion and string formatting for suggestions
18
+ """
19
+
20
+ import ast
21
+
22
+
23
+ def get_target_name(target: ast.expr) -> str:
24
+ """Get the loop variable name from AST target.
25
+
26
+ Args:
27
+ target: AST expression for loop target
28
+
29
+ Returns:
30
+ String representation of the loop variable
31
+ """
32
+ if isinstance(target, ast.Name):
33
+ return target.id
34
+ return ast.unparse(target)
35
+
36
+
37
+ def invert_condition(condition: ast.expr) -> str:
38
+ """Invert a condition (for if not x: continue -> if x).
39
+
40
+ Args:
41
+ condition: AST expression for the condition
42
+
43
+ Returns:
44
+ String representation of the inverted condition
45
+ """
46
+ if isinstance(condition, ast.UnaryOp) and isinstance(condition.op, ast.Not):
47
+ return ast.unparse(condition.operand)
48
+ return f"not ({ast.unparse(condition)})"
49
+
50
+
51
+ def build_suggestion(loop_var: str, iterable: str, conditions: list[str]) -> str:
52
+ """Generate refactoring suggestion code snippet.
53
+
54
+ Args:
55
+ loop_var: Name of the loop variable
56
+ iterable: Source representation of the iterable
57
+ conditions: List of filter conditions (already inverted)
58
+
59
+ Returns:
60
+ Code suggestion for refactoring to generator expression
61
+ """
62
+ combined = " and ".join(conditions)
63
+ return f"for {loop_var} in ({loop_var} for {loop_var} in {iterable} if {combined}):"
@@ -10,7 +10,8 @@ Overview: Provides an extensible architecture for filtering duplicate code block
10
10
 
11
11
  Dependencies: ast, re, typing
12
12
 
13
- Exports: BaseBlockFilter, BlockFilterRegistry, KeywordArgumentFilter, ImportGroupFilter
13
+ Exports: BaseBlockFilter, BlockFilterRegistry, KeywordArgumentFilter, ImportGroupFilter,
14
+ LoggerCallFilter, ExceptionReraiseFilter
14
15
 
15
16
  Interfaces: BaseBlockFilter.should_filter(code_block, file_content) -> bool
16
17
 
@@ -196,6 +197,99 @@ class ImportGroupFilter(BaseBlockFilter):
196
197
  return "import_group_filter"
197
198
 
198
199
 
200
+ class LoggerCallFilter(BaseBlockFilter):
201
+ """Filters single-line logger calls that are idiomatic but appear similar.
202
+
203
+ Detects patterns like:
204
+ logger.debug(f"Command: {cmd}")
205
+ logger.info("Starting process...")
206
+ logging.warning("...")
207
+
208
+ These are contextually different despite structural similarity.
209
+ """
210
+
211
+ def __init__(self) -> None:
212
+ """Initialize the logger call filter."""
213
+ # Pattern matches: logger.level(...) or logging.level(...)
214
+ self._logger_pattern = re.compile(
215
+ r"^\s*(self\.)?(logger|logging|log)\."
216
+ r"(debug|info|warning|error|critical|exception|log)\s*\("
217
+ )
218
+
219
+ def should_filter(self, block: CodeBlock, file_content: str) -> bool:
220
+ """Check if block is primarily single-line logger calls.
221
+
222
+ Args:
223
+ block: Code block to evaluate
224
+ file_content: Full file content
225
+
226
+ Returns:
227
+ True if block should be filtered
228
+ """
229
+ lines = file_content.split("\n")[block.start_line - 1 : block.end_line]
230
+ non_empty = [s for line in lines if (s := line.strip())]
231
+
232
+ if not non_empty:
233
+ return False
234
+
235
+ # Filter if it's a single line that's a logger call
236
+ if len(non_empty) == 1:
237
+ return bool(self._logger_pattern.match(non_empty[0]))
238
+
239
+ return False
240
+
241
+ @property
242
+ def name(self) -> str:
243
+ """Filter name."""
244
+ return "logger_call_filter"
245
+
246
+
247
+ class ExceptionReraiseFilter(BaseBlockFilter):
248
+ """Filters idiomatic exception re-raising patterns.
249
+
250
+ Detects patterns like:
251
+ except SomeError as e:
252
+ raise NewError(...) from e
253
+
254
+ These are Python best practices for exception chaining.
255
+ """
256
+
257
+ def __init__(self) -> None:
258
+ """Initialize the exception reraise filter."""
259
+ pass # Stateless filter
260
+
261
+ def should_filter(self, block: CodeBlock, file_content: str) -> bool:
262
+ """Check if block is an exception re-raise pattern.
263
+
264
+ Args:
265
+ block: Code block to evaluate
266
+ file_content: Full file content
267
+
268
+ Returns:
269
+ True if block should be filtered
270
+ """
271
+ lines = file_content.split("\n")[block.start_line - 1 : block.end_line]
272
+ stripped_lines = [s for line in lines if (s := line.strip())]
273
+
274
+ if len(stripped_lines) != 2:
275
+ return False
276
+
277
+ return self._is_except_raise_pattern(stripped_lines)
278
+
279
+ @staticmethod
280
+ def _is_except_raise_pattern(lines: list[str]) -> bool:
281
+ """Check if lines form an except/raise pattern."""
282
+ first, second = lines[0], lines[1]
283
+ is_except = first.startswith("except ") and first.endswith(":")
284
+ is_raise = second.startswith("raise ") and " from " in second
285
+ return is_except and is_raise
286
+
287
+ @property
288
+ def name(self) -> str:
289
+ """Filter name."""
290
+ return "exception_reraise_filter"
291
+
292
+
199
293
  class BlockFilterRegistry:
200
294
  """Registry for managing duplicate block filters."""
201
295
 
@@ -239,14 +333,8 @@ class BlockFilterRegistry:
239
333
  Returns:
240
334
  True if block should be filtered out
241
335
  """
242
- for filter_instance in self._filters:
243
- if filter_instance.name not in self._enabled_filters:
244
- continue
245
-
246
- if filter_instance.should_filter(block, file_content):
247
- return True
248
-
249
- return False
336
+ enabled_filters = (f for f in self._filters if f.name in self._enabled_filters)
337
+ return any(f.should_filter(block, file_content) for f in enabled_filters)
250
338
 
251
339
  def get_enabled_filters(self) -> list[str]:
252
340
  """Get list of enabled filter names.
@@ -268,5 +356,7 @@ def create_default_registry() -> BlockFilterRegistry:
268
356
  # Register built-in filters
269
357
  registry.register(KeywordArgumentFilter(threshold=DEFAULT_KEYWORD_ARG_THRESHOLD))
270
358
  registry.register(ImportGroupFilter())
359
+ registry.register(LoggerCallFilter())
360
+ registry.register(ExceptionReraiseFilter())
271
361
 
272
362
  return registry