thailint 0.2.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 (214) 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 +44 -27
  23. src/core/base.py +95 -5
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +36 -6
  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 +125 -22
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +142 -94
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +68 -21
  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 +20 -82
  73. src/linters/dry/file_analyzer.py +15 -50
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +182 -54
  76. src/linters/dry/python_analyzer.py +108 -336
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/storage_initializer.py +9 -18
  80. src/linters/dry/token_hasher.py +129 -71
  81. src/linters/dry/typescript_analyzer.py +68 -380
  82. src/linters/dry/typescript_constant_extractor.py +138 -0
  83. src/linters/dry/typescript_statement_detector.py +255 -0
  84. src/linters/dry/typescript_value_extractor.py +70 -0
  85. src/linters/dry/violation_builder.py +4 -0
  86. src/linters/dry/violation_filter.py +9 -5
  87. src/linters/dry/violation_generator.py +71 -14
  88. src/linters/file_header/__init__.py +24 -0
  89. src/linters/file_header/atemporal_detector.py +105 -0
  90. src/linters/file_header/base_parser.py +93 -0
  91. src/linters/file_header/bash_parser.py +66 -0
  92. src/linters/file_header/config.py +140 -0
  93. src/linters/file_header/css_parser.py +70 -0
  94. src/linters/file_header/field_validator.py +72 -0
  95. src/linters/file_header/linter.py +309 -0
  96. src/linters/file_header/markdown_parser.py +130 -0
  97. src/linters/file_header/python_parser.py +42 -0
  98. src/linters/file_header/typescript_parser.py +73 -0
  99. src/linters/file_header/violation_builder.py +79 -0
  100. src/linters/file_placement/config_loader.py +3 -1
  101. src/linters/file_placement/directory_matcher.py +4 -0
  102. src/linters/file_placement/linter.py +74 -31
  103. src/linters/file_placement/pattern_matcher.py +41 -6
  104. src/linters/file_placement/pattern_validator.py +31 -12
  105. src/linters/file_placement/rule_checker.py +12 -7
  106. src/linters/lazy_ignores/__init__.py +43 -0
  107. src/linters/lazy_ignores/config.py +74 -0
  108. src/linters/lazy_ignores/directive_utils.py +164 -0
  109. src/linters/lazy_ignores/header_parser.py +177 -0
  110. src/linters/lazy_ignores/linter.py +158 -0
  111. src/linters/lazy_ignores/matcher.py +168 -0
  112. src/linters/lazy_ignores/python_analyzer.py +209 -0
  113. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  114. src/linters/lazy_ignores/skip_detector.py +298 -0
  115. src/linters/lazy_ignores/types.py +71 -0
  116. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  117. src/linters/lazy_ignores/violation_builder.py +135 -0
  118. src/linters/lbyl/__init__.py +31 -0
  119. src/linters/lbyl/config.py +63 -0
  120. src/linters/lbyl/linter.py +67 -0
  121. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  122. src/linters/lbyl/pattern_detectors/base.py +63 -0
  123. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  124. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  125. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  126. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  127. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  128. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  129. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  130. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  131. src/linters/lbyl/python_analyzer.py +215 -0
  132. src/linters/lbyl/violation_builder.py +354 -0
  133. src/linters/magic_numbers/__init__.py +48 -0
  134. src/linters/magic_numbers/config.py +82 -0
  135. src/linters/magic_numbers/context_analyzer.py +249 -0
  136. src/linters/magic_numbers/linter.py +462 -0
  137. src/linters/magic_numbers/python_analyzer.py +64 -0
  138. src/linters/magic_numbers/typescript_analyzer.py +215 -0
  139. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  140. src/linters/magic_numbers/violation_builder.py +98 -0
  141. src/linters/method_property/__init__.py +49 -0
  142. src/linters/method_property/config.py +138 -0
  143. src/linters/method_property/linter.py +414 -0
  144. src/linters/method_property/python_analyzer.py +473 -0
  145. src/linters/method_property/violation_builder.py +119 -0
  146. src/linters/nesting/__init__.py +6 -2
  147. src/linters/nesting/config.py +6 -3
  148. src/linters/nesting/linter.py +31 -34
  149. src/linters/nesting/python_analyzer.py +4 -0
  150. src/linters/nesting/typescript_analyzer.py +6 -11
  151. src/linters/nesting/violation_builder.py +1 -0
  152. src/linters/performance/__init__.py +91 -0
  153. src/linters/performance/config.py +43 -0
  154. src/linters/performance/constants.py +49 -0
  155. src/linters/performance/linter.py +149 -0
  156. src/linters/performance/python_analyzer.py +365 -0
  157. src/linters/performance/regex_analyzer.py +312 -0
  158. src/linters/performance/regex_linter.py +139 -0
  159. src/linters/performance/typescript_analyzer.py +236 -0
  160. src/linters/performance/violation_builder.py +160 -0
  161. src/linters/print_statements/__init__.py +53 -0
  162. src/linters/print_statements/config.py +78 -0
  163. src/linters/print_statements/linter.py +413 -0
  164. src/linters/print_statements/python_analyzer.py +153 -0
  165. src/linters/print_statements/typescript_analyzer.py +125 -0
  166. src/linters/print_statements/violation_builder.py +96 -0
  167. src/linters/srp/__init__.py +3 -3
  168. src/linters/srp/class_analyzer.py +11 -7
  169. src/linters/srp/config.py +12 -6
  170. src/linters/srp/heuristics.py +56 -22
  171. src/linters/srp/linter.py +47 -39
  172. src/linters/srp/python_analyzer.py +55 -20
  173. src/linters/srp/typescript_metrics_calculator.py +110 -50
  174. src/linters/stateless_class/__init__.py +25 -0
  175. src/linters/stateless_class/config.py +58 -0
  176. src/linters/stateless_class/linter.py +349 -0
  177. src/linters/stateless_class/python_analyzer.py +290 -0
  178. src/linters/stringly_typed/__init__.py +36 -0
  179. src/linters/stringly_typed/config.py +189 -0
  180. src/linters/stringly_typed/context_filter.py +451 -0
  181. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  182. src/linters/stringly_typed/ignore_checker.py +100 -0
  183. src/linters/stringly_typed/ignore_utils.py +51 -0
  184. src/linters/stringly_typed/linter.py +376 -0
  185. src/linters/stringly_typed/python/__init__.py +33 -0
  186. src/linters/stringly_typed/python/analyzer.py +348 -0
  187. src/linters/stringly_typed/python/call_tracker.py +175 -0
  188. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  189. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  190. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  191. src/linters/stringly_typed/python/constants.py +21 -0
  192. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  193. src/linters/stringly_typed/python/validation_detector.py +189 -0
  194. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  195. src/linters/stringly_typed/storage.py +620 -0
  196. src/linters/stringly_typed/storage_initializer.py +45 -0
  197. src/linters/stringly_typed/typescript/__init__.py +28 -0
  198. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  199. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  200. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  201. src/linters/stringly_typed/violation_generator.py +419 -0
  202. src/orchestrator/core.py +264 -16
  203. src/orchestrator/language_detector.py +5 -3
  204. src/templates/thailint_config_template.yaml +354 -0
  205. src/utils/project_root.py +138 -16
  206. thailint-0.15.3.dist-info/METADATA +187 -0
  207. thailint-0.15.3.dist-info/RECORD +226 -0
  208. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
  209. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  210. src/cli.py +0 -1055
  211. thailint-0.2.0.dist-info/METADATA +0 -980
  212. thailint-0.2.0.dist-info/RECORD +0 -75
  213. thailint-0.2.0.dist-info/entry_points.txt +0 -4
  214. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,94 @@
1
+ """
2
+ Purpose: Builds Violation objects from CQSPattern instances
3
+
4
+ Scope: Violation message formatting and suggestion generation for CQS violations
5
+
6
+ Overview: Provides build_cqs_violation function that converts a CQSPattern with a detected
7
+ CQS violation into a Violation object with properly formatted message. Message includes
8
+ function name (with class prefix for methods), lists INPUT operations with line numbers,
9
+ lists OUTPUT operations with line numbers, and provides actionable suggestion to split
10
+ the function into separate query and command functions.
11
+
12
+ Dependencies: CQSPattern, InputOperation, OutputOperation, Violation, Severity
13
+
14
+ Exports: build_cqs_violation
15
+
16
+ Interfaces: build_cqs_violation(pattern: CQSPattern) -> Violation
17
+
18
+ Implementation: String formatting with INPUT/OUTPUT line number aggregation
19
+ """
20
+
21
+ from src.core.types import Severity, Violation
22
+
23
+ from .types import CQSPattern
24
+
25
+
26
+ def _format_inputs(pattern: CQSPattern) -> str:
27
+ """Format INPUT operations for violation message.
28
+
29
+ Args:
30
+ pattern: CQSPattern containing inputs
31
+
32
+ Returns:
33
+ Formatted string listing INPUTs with line numbers
34
+ """
35
+ if not pattern.inputs:
36
+ return ""
37
+
38
+ parts = [f"Line {inp.line}: {inp.target} = {inp.expression}" for inp in pattern.inputs]
39
+ return "; ".join(parts)
40
+
41
+
42
+ def _format_outputs(pattern: CQSPattern) -> str:
43
+ """Format OUTPUT operations for violation message.
44
+
45
+ Args:
46
+ pattern: CQSPattern containing outputs
47
+
48
+ Returns:
49
+ Formatted string listing OUTPUTs with line numbers
50
+ """
51
+ if not pattern.outputs:
52
+ return ""
53
+
54
+ parts = [f"Line {out.line}: {out.expression}" for out in pattern.outputs]
55
+ return "; ".join(parts)
56
+
57
+
58
+ def build_cqs_violation(pattern: CQSPattern) -> Violation:
59
+ """Build a Violation object from a CQSPattern.
60
+
61
+ Creates a violation message that includes:
62
+ - Function name (with class prefix for methods)
63
+ - List of INPUT operations with line numbers
64
+ - List of OUTPUT operations with line numbers
65
+ - Suggestion to split into query and command functions
66
+
67
+ Args:
68
+ pattern: CQSPattern representing a function that violates CQS
69
+
70
+ Returns:
71
+ Violation object with formatted message and suggestion
72
+ """
73
+ full_name = pattern.get_full_name()
74
+
75
+ # Build detailed message
76
+ inputs_str = _format_inputs(pattern)
77
+ outputs_str = _format_outputs(pattern)
78
+
79
+ message = (
80
+ f"Function '{full_name}' violates CQS: mixes queries and commands. "
81
+ f"INPUTs: {inputs_str}. OUTPUTs: {outputs_str}."
82
+ )
83
+
84
+ suggestion = "Split into separate query and command functions."
85
+
86
+ return Violation(
87
+ rule_id="cqs",
88
+ file_path=pattern.file_path,
89
+ line=pattern.line,
90
+ column=pattern.column,
91
+ message=message,
92
+ severity=Severity.ERROR,
93
+ suggestion=suggestion,
94
+ )
@@ -9,28 +9,35 @@ Overview: Provides shared infrastructure for token-based duplicate code detectio
9
9
  for TypeScript). Eliminates duplication between PythonDuplicateAnalyzer and TypeScriptDuplicateAnalyzer
10
10
  by extracting shared analyze() method pattern and CodeBlock creation logic.
11
11
 
12
- Dependencies: TokenHasher, CodeBlock, DRYConfig, pathlib.Path
12
+ Dependencies: token_hasher module functions, CodeBlock, DRYConfig, pathlib.Path
13
13
 
14
14
  Exports: BaseTokenAnalyzer class
15
15
 
16
16
  Interfaces: BaseTokenAnalyzer.analyze(file_path: Path, content: str, config: DRYConfig) -> list[CodeBlock]
17
17
 
18
18
  Implementation: Template method pattern with extension point for language-specific block filtering
19
+
20
+ Suppressions:
21
+ - stateless-class: BaseTokenAnalyzer is an intentional template method base class.
22
+ Subclasses (PythonDuplicateAnalyzer, TypeScriptDuplicateAnalyzer) override
23
+ _should_include_block for language-specific filtering. Statelessness is by design
24
+ since state was moved to module-level functions in token_hasher.
19
25
  """
20
26
 
21
27
  from pathlib import Path
22
28
 
29
+ from . import token_hasher
23
30
  from .cache import CodeBlock
24
31
  from .config import DRYConfig
25
- from .token_hasher import TokenHasher
26
32
 
27
33
 
28
- class BaseTokenAnalyzer:
29
- """Base analyzer for token-based duplicate detection."""
34
+ class BaseTokenAnalyzer: # thailint: ignore[stateless-class] - Template method base class for inheritance
35
+ """Base analyzer for token-based duplicate detection.
30
36
 
31
- def __init__(self) -> None:
32
- """Initialize analyzer with token hasher."""
33
- self._hasher = TokenHasher()
37
+ This is intentionally a base class for polymorphism. Subclasses
38
+ (PythonDuplicateAnalyzer, TypeScriptDuplicateAnalyzer) override
39
+ _should_include_block for language-specific filtering.
40
+ """
34
41
 
35
42
  def analyze(self, file_path: Path, content: str, config: DRYConfig) -> list[CodeBlock]:
36
43
  """Analyze file for duplicate code blocks.
@@ -43,8 +50,8 @@ class BaseTokenAnalyzer:
43
50
  Returns:
44
51
  List of CodeBlock instances with hash values
45
52
  """
46
- lines = self._hasher.tokenize(content)
47
- windows = self._hasher.rolling_hash(lines, config.min_duplicate_lines)
53
+ lines = token_hasher.tokenize(content)
54
+ windows = token_hasher.rolling_hash(lines, config.min_duplicate_lines)
48
55
 
49
56
  blocks = []
50
57
  for hash_val, start_line, end_line, snippet in windows:
@@ -10,11 +10,15 @@ 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
 
17
18
  Implementation: Strategy pattern with filter registry for extensibility
19
+
20
+ Suppressions:
21
+ - type:ignore[operator]: Tree-sitter Node comparison operations (optional dependency)
18
22
  """
19
23
 
20
24
  import ast
@@ -23,6 +27,9 @@ from abc import ABC, abstractmethod
23
27
  from pathlib import Path
24
28
  from typing import Protocol
25
29
 
30
+ # Default filter threshold constants
31
+ DEFAULT_KEYWORD_ARG_THRESHOLD = 0.8
32
+
26
33
 
27
34
  class CodeBlock(Protocol):
28
35
  """Protocol for code blocks (matches cache.CodeBlock)."""
@@ -50,9 +57,10 @@ class BaseBlockFilter(ABC):
50
57
  """
51
58
  pass
52
59
 
60
+ @property
53
61
  @abstractmethod
54
- def get_name(self) -> str:
55
- """Get filter name for configuration and logging."""
62
+ def name(self) -> str:
63
+ """Filter name for configuration and logging."""
56
64
  pass
57
65
 
58
66
 
@@ -67,7 +75,7 @@ class KeywordArgumentFilter(BaseBlockFilter):
67
75
  These are common in builder patterns and API calls.
68
76
  """
69
77
 
70
- def __init__(self, threshold: float = 0.8):
78
+ def __init__(self, threshold: float = DEFAULT_KEYWORD_ARG_THRESHOLD):
71
79
  """Initialize filter.
72
80
 
73
81
  Args:
@@ -110,10 +118,10 @@ class KeywordArgumentFilter(BaseBlockFilter):
110
118
  return False
111
119
 
112
120
  # Find if any Call node contains the block
113
- for node in ast.walk(tree):
114
- if isinstance(node, ast.Call) and self._check_multiline_containment(node, block):
115
- return True
116
- return False
121
+ return any(
122
+ isinstance(node, ast.Call) and self._check_multiline_containment(node, block)
123
+ for node in ast.walk(tree)
124
+ )
117
125
 
118
126
  @staticmethod
119
127
  def _check_multiline_containment(node: ast.Call, block: CodeBlock) -> bool:
@@ -149,8 +157,9 @@ class KeywordArgumentFilter(BaseBlockFilter):
149
157
  return False
150
158
  return True
151
159
 
152
- def get_name(self) -> str:
153
- """Get filter name."""
160
+ @property
161
+ def name(self) -> str:
162
+ """Filter name."""
154
163
  return "keyword_argument_filter"
155
164
 
156
165
 
@@ -160,6 +169,10 @@ class ImportGroupFilter(BaseBlockFilter):
160
169
  Import organization often creates similar patterns that aren't meaningful duplication.
161
170
  """
162
171
 
172
+ def __init__(self) -> None:
173
+ """Initialize the import group filter."""
174
+ pass # Stateless filter for import blocks
175
+
163
176
  def should_filter(self, block: CodeBlock, file_content: str) -> bool:
164
177
  """Check if block is only import statements.
165
178
 
@@ -181,11 +194,105 @@ class ImportGroupFilter(BaseBlockFilter):
181
194
 
182
195
  return True
183
196
 
184
- def get_name(self) -> str:
185
- """Get filter name."""
197
+ @property
198
+ def name(self) -> str:
199
+ """Filter name."""
186
200
  return "import_group_filter"
187
201
 
188
202
 
203
+ class LoggerCallFilter(BaseBlockFilter):
204
+ """Filters single-line logger calls that are idiomatic but appear similar.
205
+
206
+ Detects patterns like:
207
+ logger.debug(f"Command: {cmd}")
208
+ logger.info("Starting process...")
209
+ logging.warning("...")
210
+
211
+ These are contextually different despite structural similarity.
212
+ """
213
+
214
+ def __init__(self) -> None:
215
+ """Initialize the logger call filter."""
216
+ # Pattern matches: logger.level(...) or logging.level(...)
217
+ self._logger_pattern = re.compile(
218
+ r"^\s*(self\.)?(logger|logging|log)\."
219
+ r"(debug|info|warning|error|critical|exception|log)\s*\("
220
+ )
221
+
222
+ def should_filter(self, block: CodeBlock, file_content: str) -> bool:
223
+ """Check if block is primarily single-line logger calls.
224
+
225
+ Args:
226
+ block: Code block to evaluate
227
+ file_content: Full file content
228
+
229
+ Returns:
230
+ True if block should be filtered
231
+ """
232
+ lines = file_content.split("\n")[block.start_line - 1 : block.end_line]
233
+ non_empty = [s for line in lines if (s := line.strip())]
234
+
235
+ if not non_empty:
236
+ return False
237
+
238
+ # Filter if it's a single line that's a logger call
239
+ if len(non_empty) == 1:
240
+ return bool(self._logger_pattern.match(non_empty[0]))
241
+
242
+ return False
243
+
244
+ @property
245
+ def name(self) -> str:
246
+ """Filter name."""
247
+ return "logger_call_filter"
248
+
249
+
250
+ class ExceptionReraiseFilter(BaseBlockFilter):
251
+ """Filters idiomatic exception re-raising patterns.
252
+
253
+ Detects patterns like:
254
+ except SomeError as e:
255
+ raise NewError(...) from e
256
+
257
+ These are Python best practices for exception chaining.
258
+ """
259
+
260
+ def __init__(self) -> None:
261
+ """Initialize the exception reraise filter."""
262
+ pass # Stateless filter
263
+
264
+ def should_filter(self, block: CodeBlock, file_content: str) -> bool:
265
+ """Check if block is an exception re-raise pattern.
266
+
267
+ Args:
268
+ block: Code block to evaluate
269
+ file_content: Full file content
270
+
271
+ Returns:
272
+ True if block should be filtered
273
+ """
274
+ lines = file_content.split("\n")[block.start_line - 1 : block.end_line]
275
+ stripped_lines = [s for line in lines if (s := line.strip())]
276
+
277
+ if len(stripped_lines) != 2:
278
+ return False
279
+
280
+ return self._is_except_raise_pattern(stripped_lines)
281
+
282
+ @staticmethod
283
+ def _is_except_raise_pattern(lines: list[str]) -> bool:
284
+ """Check if lines form an except/raise pattern."""
285
+ first, second = lines[0], lines[1]
286
+ is_except = first.startswith("except ") and first.endswith(":")
287
+ is_raise = second.startswith("raise ") and " from " in second
288
+ return is_except and is_raise
289
+
290
+ @property
291
+ def name(self) -> str:
292
+ """Filter name."""
293
+ return "exception_reraise_filter"
294
+
295
+
189
296
  class BlockFilterRegistry:
190
297
  """Registry for managing duplicate block filters."""
191
298
 
@@ -201,7 +308,7 @@ class BlockFilterRegistry:
201
308
  filter_instance: Filter to register
202
309
  """
203
310
  self._filters.append(filter_instance)
204
- self._enabled_filters.add(filter_instance.get_name())
311
+ self._enabled_filters.add(filter_instance.name)
205
312
 
206
313
  def enable_filter(self, filter_name: str) -> None:
207
314
  """Enable a specific filter by name.
@@ -229,14 +336,8 @@ class BlockFilterRegistry:
229
336
  Returns:
230
337
  True if block should be filtered out
231
338
  """
232
- for filter_instance in self._filters:
233
- if filter_instance.get_name() not in self._enabled_filters:
234
- continue
235
-
236
- if filter_instance.should_filter(block, file_content):
237
- return True
238
-
239
- return False
339
+ enabled_filters = (f for f in self._filters if f.name in self._enabled_filters)
340
+ return any(f.should_filter(block, file_content) for f in enabled_filters)
240
341
 
241
342
  def get_enabled_filters(self) -> list[str]:
242
343
  """Get list of enabled filter names.
@@ -256,7 +357,9 @@ def create_default_registry() -> BlockFilterRegistry:
256
357
  registry = BlockFilterRegistry()
257
358
 
258
359
  # Register built-in filters
259
- registry.register(KeywordArgumentFilter(threshold=0.8))
360
+ registry.register(KeywordArgumentFilter(threshold=DEFAULT_KEYWORD_ARG_THRESHOLD))
260
361
  registry.register(ImportGroupFilter())
362
+ registry.register(LoggerCallFilter())
363
+ registry.register(ExceptionReraiseFilter())
261
364
 
262
365
  return registry
@@ -26,6 +26,10 @@ from .cache import CodeBlock
26
26
  class BlockGrouper:
27
27
  """Groups blocks and violations by file path."""
28
28
 
29
+ def __init__(self) -> None:
30
+ """Initialize the block grouper."""
31
+ pass # Stateless grouper for code blocks
32
+
29
33
  def group_blocks_by_file(self, blocks: list[CodeBlock]) -> dict[Path, list[CodeBlock]]:
30
34
  """Group blocks by file path.
31
35