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.
Files changed (204) 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 +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  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 +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  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 +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
src/linters/dry/linter.py CHANGED
@@ -1,47 +1,59 @@
1
1
  """
2
2
  Purpose: Main DRY linter rule implementation with stateful caching
3
3
 
4
- Scope: DRYRule class implementing BaseLintRule interface for duplicate code detection
4
+ Scope: DRYRule class implementing BaseLintRule interface for duplicate code and constant detection
5
5
 
6
6
  Overview: Implements DRY linter rule following BaseLintRule interface with stateful caching design.
7
7
  Orchestrates duplicate detection by delegating to specialized classes: ConfigLoader for config,
8
8
  StorageInitializer for storage setup, FileAnalyzer for file analysis, and ViolationGenerator
9
- for violation creation. Maintains minimal orchestration logic to comply with SRP (8 methods total).
9
+ for violation creation. Also supports duplicate constant detection (opt-in) to identify when
10
+ the same constant is defined in multiple files. Maintains minimal orchestration logic to comply
11
+ with SRP.
10
12
 
11
13
  Dependencies: BaseLintRule, BaseLintContext, ConfigLoader, StorageInitializer, FileAnalyzer,
12
- DuplicateStorage, ViolationGenerator
14
+ DuplicateStorage, ViolationGenerator, extract_python_constants, TypeScriptConstantExtractor,
15
+ find_constant_groups, ConstantViolationBuilder
13
16
 
14
17
  Exports: DRYRule class
15
18
 
16
19
  Interfaces: DRYRule.check(context) -> list[Violation], finalize() -> list[Violation]
17
20
 
18
21
  Implementation: Delegates all logic to helper classes, maintains only orchestration and state
22
+
23
+ Suppressions:
24
+ - too-many-instance-attributes: DRYComponents groups helper dependencies; DRYRule has 8
25
+ attributes due to stateful caching requirements (storage, config, constants, file contents
26
+ for ignore directive processing)
27
+ - B101: Type narrowing assertions after guards (storage initialized, file_path/content set)
19
28
  """
20
29
 
21
30
  from __future__ import annotations
22
31
 
32
+ from collections.abc import Callable
23
33
  from dataclasses import dataclass
24
34
  from pathlib import Path
25
- from typing import TYPE_CHECKING
26
35
 
27
36
  from src.core.base import BaseLintContext, BaseLintRule
28
37
  from src.core.linter_utils import should_process_file
29
38
  from src.core.types import Violation
39
+ from src.linter_config.ignore import IgnoreDirectiveParser
30
40
 
31
41
  from .config import DRYConfig
32
42
  from .config_loader import ConfigLoader
43
+ from .constant import ConstantInfo
44
+ from .constant_matcher import find_constant_groups
45
+ from .constant_violation_builder import ConstantViolationBuilder
33
46
  from .duplicate_storage import DuplicateStorage
34
47
  from .file_analyzer import FileAnalyzer
35
48
  from .inline_ignore import InlineIgnoreParser
49
+ from .python_constant_extractor import extract_python_constants
36
50
  from .storage_initializer import StorageInitializer
37
- from .violation_generator import ViolationGenerator
38
-
39
- if TYPE_CHECKING:
40
- from .cache import CodeBlock
51
+ from .typescript_constant_extractor import TypeScriptConstantExtractor
52
+ from .violation_generator import IgnoreContext, ViolationGenerator
41
53
 
42
54
 
43
55
  @dataclass
44
- class DRYComponents:
56
+ class DRYComponents: # pylint: disable=too-many-instance-attributes
45
57
  """Component dependencies for DRY linter."""
46
58
 
47
59
  config_loader: ConfigLoader
@@ -49,9 +61,11 @@ class DRYComponents:
49
61
  file_analyzer: FileAnalyzer
50
62
  violation_generator: ViolationGenerator
51
63
  inline_ignore: InlineIgnoreParser
64
+ typescript_extractor: TypeScriptConstantExtractor
65
+ constant_violation_builder: ConstantViolationBuilder
52
66
 
53
67
 
54
- class DRYRule(BaseLintRule):
68
+ class DRYRule(BaseLintRule): # pylint: disable=too-many-instance-attributes
55
69
  """Detects duplicate code across project files."""
56
70
 
57
71
  def __init__(self) -> None:
@@ -60,6 +74,13 @@ class DRYRule(BaseLintRule):
60
74
  self._initialized = False
61
75
  self._config: DRYConfig | None = None
62
76
  self._file_analyzer: FileAnalyzer | None = None
77
+ self._project_root: Path | None = None
78
+
79
+ # Collected constants for cross-file detection: list of (file_path, ConstantInfo)
80
+ self._constants: list[tuple[Path, ConstantInfo]] = []
81
+
82
+ # Cache file contents for ignore directive checking during finalize
83
+ self._file_contents: dict[str, str] = {}
63
84
 
64
85
  # Helper components grouped to reduce instance attributes
65
86
  self._helpers = DRYComponents(
@@ -68,8 +89,22 @@ class DRYRule(BaseLintRule):
68
89
  file_analyzer=FileAnalyzer(), # Placeholder, will be replaced with configured one
69
90
  violation_generator=ViolationGenerator(),
70
91
  inline_ignore=InlineIgnoreParser(),
92
+ typescript_extractor=TypeScriptConstantExtractor(),
93
+ constant_violation_builder=ConstantViolationBuilder(),
71
94
  )
72
95
 
96
+ @property
97
+ def _active_storage(self) -> DuplicateStorage:
98
+ """Get storage, asserting it has been initialized."""
99
+ assert self._storage is not None, "Storage not initialized" # nosec B101
100
+ return self._storage
101
+
102
+ @property
103
+ def _active_file_analyzer(self) -> FileAnalyzer:
104
+ """Get file analyzer, asserting it has been initialized."""
105
+ assert self._file_analyzer is not None, "File analyzer not initialized" # nosec B101
106
+ return self._file_analyzer
107
+
73
108
  @property
74
109
  def rule_id(self) -> str:
75
110
  """Unique identifier for this rule."""
@@ -86,14 +121,7 @@ class DRYRule(BaseLintRule):
86
121
  return "Detects duplicate code blocks across the project"
87
122
 
88
123
  def check(self, context: BaseLintContext) -> list[Violation]:
89
- """Analyze file and store blocks (collection phase).
90
-
91
- Args:
92
- context: Lint context with file information
93
-
94
- Returns:
95
- Empty list (violations returned in finalize())
96
- """
124
+ """Analyze file and store blocks (collection phase)."""
97
125
  if not should_process_file(context):
98
126
  return []
99
127
 
@@ -101,18 +129,28 @@ class DRYRule(BaseLintRule):
101
129
  if not config.enabled:
102
130
  return []
103
131
 
104
- # Store config for finalize()
105
- if self._config is None:
106
- self._config = config
132
+ self._config = self._config or config
133
+ self._process_file(context, config)
134
+ return []
135
+
136
+ def _process_file(self, context: BaseLintContext, config: DRYConfig) -> None:
137
+ """Process a single file for duplicates and constants."""
138
+ # should_process_file ensures file_path and file_content are set
139
+ assert context.file_path is not None # nosec B101
140
+ assert context.file_content is not None # nosec B101
107
141
 
108
- # Parse inline ignore directives from this file
109
- file_path = Path(context.file_path) # type: ignore[arg-type]
110
- self._helpers.inline_ignore.parse_file(file_path, context.file_content or "")
142
+ file_path = context.file_path
143
+ # Cache file content for ignore directive checking in finalize
144
+ self._file_contents[str(file_path)] = context.file_content
145
+ # Get project root from context metadata if available
146
+ if self._project_root is None:
147
+ self._project_root = self._get_project_root(context)
111
148
 
149
+ self._helpers.inline_ignore.parse_file(file_path, context.file_content)
112
150
  self._ensure_storage_initialized(context, config)
113
151
  self._analyze_and_store(context, config)
114
-
115
- return []
152
+ if config.detect_duplicate_constants:
153
+ self._extract_and_store_constants(context)
116
154
 
117
155
  def _ensure_storage_initialized(self, context: BaseLintContext, config: DRYConfig) -> None:
118
156
  """Initialize storage and file analyzer on first call."""
@@ -124,40 +162,137 @@ class DRYRule(BaseLintRule):
124
162
 
125
163
  def _analyze_and_store(self, context: BaseLintContext, config: DRYConfig) -> None:
126
164
  """Analyze file and store blocks."""
127
- # Guaranteed by _should_process_file check
128
- if context.file_path is None or context.file_content is None:
129
- return # Should never happen due to should_process_file check
130
-
131
- if not self._file_analyzer:
132
- return # Should never happen after initialization
133
-
134
- file_path = Path(context.file_path)
135
- blocks = self._file_analyzer.analyze(
136
- file_path, context.file_content, context.language, config
165
+ if not self._can_analyze(context):
166
+ return
167
+ # _can_analyze ensures file_path and file_content are set
168
+ assert context.file_path is not None # nosec B101
169
+ assert context.file_content is not None # nosec B101
170
+
171
+ blocks = self._active_file_analyzer.analyze(
172
+ context.file_path,
173
+ context.file_content,
174
+ context.language,
175
+ config,
137
176
  )
138
-
139
177
  if blocks:
140
- self._store_blocks(file_path, blocks)
178
+ self._active_storage.add_blocks(context.file_path, blocks)
179
+
180
+ def _can_analyze(self, context: BaseLintContext) -> bool:
181
+ """Check if context is ready for analysis."""
182
+ return (
183
+ context.file_path is not None
184
+ and context.file_content is not None
185
+ and self._file_analyzer is not None
186
+ and self._storage is not None
187
+ )
141
188
 
142
- def _store_blocks(self, file_path: Path, blocks: list[CodeBlock]) -> None:
143
- """Store blocks in SQLite if storage available."""
144
- if self._storage:
145
- self._storage.add_blocks(file_path, blocks)
189
+ def _extract_and_store_constants(self, context: BaseLintContext) -> None:
190
+ """Extract constants from file and store for cross-file detection."""
191
+ if context.file_path is None or context.file_content is None:
192
+ return
193
+ file_path = Path(context.file_path)
194
+ extract_fn = _get_extractor_for_language(context.language, self._helpers)
195
+ if extract_fn:
196
+ self._constants.extend((file_path, c) for c in extract_fn(context.file_content))
146
197
 
147
- def finalize(self) -> list[Violation]:
148
- """Generate violations after all files processed.
198
+ def _get_project_root(self, context: BaseLintContext) -> Path | None:
199
+ """Get project root from context if available.
200
+
201
+ Args:
202
+ context: Lint context
149
203
 
150
204
  Returns:
151
- List of all violations found across all files
205
+ Project root path or None if not available
152
206
  """
207
+ # Try to get from metadata (orchestrator sets this)
208
+ if hasattr(context, "metadata") and isinstance(context.metadata, dict):
209
+ project_root = context.metadata.get("project_root")
210
+ if project_root:
211
+ return Path(project_root)
212
+
213
+ # Fallback: derive from file path
214
+ if context.file_path:
215
+ return Path(context.file_path).parent
216
+
217
+ return None
218
+
219
+ def finalize(self) -> list[Violation]:
220
+ """Generate violations after all files processed."""
153
221
  if not self._storage or not self._config:
154
222
  return []
155
223
 
224
+ # Create ignore context for violation filtering
225
+ ignore_parser = IgnoreDirectiveParser(self._project_root)
226
+ ignore_ctx = IgnoreContext(
227
+ inline_ignore=self._helpers.inline_ignore,
228
+ shared_parser=ignore_parser,
229
+ file_contents=self._file_contents,
230
+ )
231
+
156
232
  violations = self._helpers.violation_generator.generate_violations(
157
- self._storage, self.rule_id, self._config, self._helpers.inline_ignore
233
+ self._storage, self.rule_id, self._config, ignore_ctx
158
234
  )
235
+ if self._config.detect_duplicate_constants and self._constants:
236
+ constant_violations = _generate_constant_violations(
237
+ self._constants, self._config, self._helpers, self.rule_id
238
+ )
239
+ # Filter constant violations through shared ignore parser
240
+ constant_violations = _filter_ignored_violations(
241
+ constant_violations, ignore_parser, self._file_contents
242
+ )
243
+ violations.extend(constant_violations)
159
244
 
160
- # Clear inline ignore cache for next run
161
245
  self._helpers.inline_ignore.clear()
162
-
246
+ self._constants = []
247
+ self._file_contents = {}
163
248
  return violations
249
+
250
+
251
+ ConstantExtractorFn = Callable[[str], list[ConstantInfo]]
252
+
253
+
254
+ def _get_extractor_for_language(
255
+ language: str | None, helpers: DRYComponents
256
+ ) -> ConstantExtractorFn | None:
257
+ """Get the appropriate constant extractor function for a language."""
258
+ extractors: dict[str, ConstantExtractorFn] = {
259
+ "python": extract_python_constants,
260
+ "typescript": helpers.typescript_extractor.extract,
261
+ "javascript": helpers.typescript_extractor.extract,
262
+ }
263
+ return extractors.get(language or "")
264
+
265
+
266
+ def _generate_constant_violations(
267
+ constants: list[tuple[Path, ConstantInfo]],
268
+ config: DRYConfig,
269
+ helpers: DRYComponents,
270
+ rule_id: str,
271
+ ) -> list[Violation]:
272
+ """Generate violations for duplicate constants."""
273
+ groups = find_constant_groups(constants)
274
+ helpers.constant_violation_builder.min_occurrences = config.min_constant_occurrences
275
+ return helpers.constant_violation_builder.build_violations(groups, rule_id)
276
+
277
+
278
+ def _filter_ignored_violations(
279
+ violations: list[Violation],
280
+ ignore_parser: IgnoreDirectiveParser,
281
+ file_contents: dict[str, str],
282
+ ) -> list[Violation]:
283
+ """Filter violations through the shared ignore directive parser.
284
+
285
+ Args:
286
+ violations: List of violations to filter
287
+ ignore_parser: Shared ignore directive parser
288
+ file_contents: Cached file contents for checking ignore directives
289
+
290
+ Returns:
291
+ Filtered list of violations not matching ignore directives
292
+ """
293
+ filtered = []
294
+ for violation in violations:
295
+ file_content = file_contents.get(violation.file_path, "")
296
+ if not ignore_parser.should_ignore_violation(violation, file_content):
297
+ filtered.append(violation)
298
+ return filtered