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
@@ -0,0 +1,419 @@
1
+ """
2
+ Purpose: Violation generation from cross-file stringly-typed patterns
3
+
4
+ Scope: Generates violations from duplicate pattern hashes, function call patterns, and
5
+ scattered string comparisons
6
+
7
+ Overview: Handles violation generation for stringly-typed patterns that appear across multiple
8
+ files. Queries storage for duplicate hashes, retrieves patterns for each hash, builds
9
+ violations with cross-references to other files, and filters patterns based on enum value
10
+ thresholds. Delegates function call violation generation to FunctionCallViolationBuilder.
11
+ Generates violations for scattered string comparisons (e.g., `if env == "production"`)
12
+ where a variable is compared to multiple unique string values across files.
13
+ Applies inline ignore directives via IgnoreChecker to filter suppressed violations.
14
+ Separates violation generation logic from main linter rule to maintain SRP compliance.
15
+
16
+ Dependencies: StringlyTypedStorage, StoredPattern, StoredComparison, StringlyTypedConfig,
17
+ Violation, Severity, build_function_call_violations, IgnoreChecker
18
+
19
+ Exports: ViolationGenerator class, pure helper functions for message building
20
+
21
+ Interfaces: ViolationGenerator.generate_violations(storage, rule_id, config) -> list[Violation]
22
+
23
+ Implementation: Queries storage, validates pattern thresholds, builds violations with
24
+ cross-file references, delegates function call violations to builder, generates
25
+ comparison violations from scattered string comparisons, filters by ignore directives
26
+
27
+ Suppressions:
28
+ - too-many-arguments,too-many-positional-arguments: _process_variable helper passes
29
+ accumulated state (storage, config, covered_variables, violations) to avoid
30
+ global state or complex return types
31
+ """
32
+
33
+ from src.core.types import Severity, Violation
34
+
35
+ from . import context_filter
36
+ from .config import StringlyTypedConfig
37
+ from .function_call_violation_builder import build_function_call_violations
38
+ from .ignore_checker import IgnoreChecker
39
+ from .ignore_utils import is_ignored
40
+ from .storage import StoredComparison, StoredPattern, StringlyTypedStorage
41
+
42
+ # --- Pure helper functions for filtering ---
43
+
44
+
45
+ def _filter_by_ignore(violations: list[Violation], ignore: list[str]) -> list[Violation]:
46
+ """Filter violations by ignore patterns."""
47
+ if not ignore:
48
+ return violations
49
+ return [v for v in violations if not is_ignored(v.file_path, ignore)]
50
+
51
+
52
+ def _is_allowed_value_set(values: set[str], config: StringlyTypedConfig) -> bool:
53
+ """Check if a set of values is in the allowed list."""
54
+ return any(values == set(allowed) for allowed in config.allowed_string_sets)
55
+
56
+
57
+ def _has_spaces(values: set[str]) -> bool:
58
+ """Check if any value contains spaces (indicates SQL/templates, not enums)."""
59
+ return any(" " in v for v in values)
60
+
61
+
62
+ def _is_enum_candidate(pattern: StoredPattern, config: StringlyTypedConfig) -> bool:
63
+ """Check if pattern's value count is within enum range."""
64
+ value_count = len(pattern.string_values)
65
+ return config.min_values_for_enum <= value_count <= config.max_values_for_enum
66
+
67
+
68
+ def _is_pattern_allowed(pattern: StoredPattern, config: StringlyTypedConfig) -> bool:
69
+ """Check if pattern's string set is in allowed list."""
70
+ return _is_allowed_value_set(set(pattern.string_values), config)
71
+
72
+
73
+ def _should_skip_pattern_values(values: set[str], config: StringlyTypedConfig) -> bool:
74
+ """Check if pattern values should be skipped."""
75
+ return (
76
+ _is_allowed_value_set(values, config)
77
+ or context_filter.are_all_values_excluded(values)
78
+ or _has_spaces(values)
79
+ )
80
+
81
+
82
+ def _should_skip_patterns(patterns: list[StoredPattern], config: StringlyTypedConfig) -> bool:
83
+ """Check if pattern group should be skipped based on config."""
84
+ if not patterns:
85
+ return True
86
+ first = patterns[0]
87
+ if not _is_enum_candidate(first, config):
88
+ return True
89
+ return _should_skip_pattern_values(set(first.string_values), config)
90
+
91
+
92
+ def _should_skip_comparison(unique_values: set[str], config: StringlyTypedConfig) -> bool:
93
+ """Check if a comparison pattern should be skipped based on config."""
94
+ if len(unique_values) > config.max_values_for_enum:
95
+ return True
96
+ if _is_allowed_value_set(unique_values, config):
97
+ return True
98
+ if context_filter.are_all_values_excluded(unique_values):
99
+ return True
100
+ if _has_spaces(unique_values):
101
+ return True
102
+ return False
103
+
104
+
105
+ # Variable suffixes that indicate false positive comparisons
106
+ _EXCLUDED_VARIABLE_SUFFIXES: tuple[str, ...] = (
107
+ ".value", # Enum value access
108
+ ".method", # HTTP method
109
+ ".type", # Tree-sitter node types
110
+ )
111
+
112
+
113
+ def _should_skip_variable(variable_name: str) -> bool:
114
+ """Check if a variable name indicates a false positive comparison."""
115
+ # Check excluded suffixes
116
+ if any(variable_name.endswith(s) for s in _EXCLUDED_VARIABLE_SUFFIXES):
117
+ return True
118
+ # Test assertion patterns (underscore prefix is common in comprehensions/lambdas)
119
+ return variable_name.startswith("_.")
120
+
121
+
122
+ # --- Pure helper functions for message building ---
123
+
124
+
125
+ def _build_cross_references(pattern: StoredPattern, all_patterns: list[StoredPattern]) -> str:
126
+ """Build cross-reference string for other files."""
127
+ refs = [
128
+ f"{other.file_path.name}:{other.line_number}"
129
+ for other in all_patterns
130
+ if other.file_path != pattern.file_path
131
+ ]
132
+ return ", ".join(refs)
133
+
134
+
135
+ def _build_message(pattern: StoredPattern, all_patterns: list[StoredPattern]) -> str:
136
+ """Build violation message with cross-file references."""
137
+ file_count = len({p.file_path for p in all_patterns})
138
+ values_str = ", ".join(f"'{v}'" for v in sorted(pattern.string_values))
139
+ other_refs = _build_cross_references(pattern, all_patterns)
140
+
141
+ message = f"Stringly-typed pattern with values [{values_str}] appears in {file_count} files."
142
+ if other_refs:
143
+ message += f" Also found in: {other_refs}."
144
+
145
+ return message
146
+
147
+
148
+ def _build_suggestion(pattern: StoredPattern) -> str:
149
+ """Build fix suggestion for the pattern."""
150
+ values_count = len(pattern.string_values)
151
+ var_info = f" for '{pattern.variable_name}'" if pattern.variable_name else ""
152
+
153
+ return (
154
+ f"Consider defining an enum or type union{var_info} with the "
155
+ f"{values_count} possible values instead of using string literals."
156
+ )
157
+
158
+
159
+ def _build_comparison_cross_references(
160
+ comparison: StoredComparison,
161
+ all_comparisons: list[StoredComparison],
162
+ ) -> str:
163
+ """Build cross-reference string for other comparison locations."""
164
+ refs = [
165
+ f"{other.file_path.name}:{other.line_number}"
166
+ for other in all_comparisons
167
+ if other.file_path != comparison.file_path
168
+ ]
169
+ return ", ".join(refs)
170
+
171
+
172
+ def _build_comparison_message(
173
+ comparison: StoredComparison,
174
+ all_comparisons: list[StoredComparison],
175
+ unique_values: set[str],
176
+ ) -> str:
177
+ """Build violation message for scattered comparison."""
178
+ file_count = len({c.file_path for c in all_comparisons})
179
+ values_str = ", ".join(f"'{v}'" for v in sorted(unique_values))
180
+ other_refs = _build_comparison_cross_references(comparison, all_comparisons)
181
+
182
+ message = (
183
+ f"Variable '{comparison.variable_name}' is compared to {len(unique_values)} "
184
+ f"different string values [{values_str}] across {file_count} file(s)."
185
+ )
186
+ if other_refs:
187
+ message += f" Also compared in: {other_refs}."
188
+
189
+ return message
190
+
191
+
192
+ def _build_comparison_suggestion(comparison: StoredComparison, unique_values: set[str]) -> str:
193
+ """Build fix suggestion for scattered comparison."""
194
+ return (
195
+ f"Consider defining an enum for '{comparison.variable_name}' with the "
196
+ f"{len(unique_values)} possible values instead of using string literals "
197
+ f"in scattered comparisons."
198
+ )
199
+
200
+
201
+ # --- Pure helper function for violation building ---
202
+
203
+
204
+ def _build_violation(
205
+ pattern: StoredPattern, all_patterns: list[StoredPattern], rule_id: str
206
+ ) -> Violation:
207
+ """Build a violation for a pattern with cross-references."""
208
+ message = _build_message(pattern, all_patterns)
209
+ suggestion = _build_suggestion(pattern)
210
+
211
+ return Violation(
212
+ rule_id=rule_id,
213
+ file_path=str(pattern.file_path),
214
+ line=pattern.line_number,
215
+ column=pattern.column,
216
+ message=message,
217
+ severity=Severity.ERROR,
218
+ suggestion=suggestion,
219
+ )
220
+
221
+
222
+ def _build_comparison_violation(
223
+ comparison: StoredComparison,
224
+ all_comparisons: list[StoredComparison],
225
+ unique_values: set[str],
226
+ ) -> Violation:
227
+ """Build a violation for a scattered string comparison."""
228
+ message = _build_comparison_message(comparison, all_comparisons, unique_values)
229
+ suggestion = _build_comparison_suggestion(comparison, unique_values)
230
+
231
+ return Violation(
232
+ rule_id="stringly-typed.scattered-comparison",
233
+ file_path=str(comparison.file_path),
234
+ line=comparison.line_number,
235
+ column=comparison.column,
236
+ message=message,
237
+ severity=Severity.ERROR,
238
+ suggestion=suggestion,
239
+ )
240
+
241
+
242
+ # --- Helper functions for pattern processing ---
243
+
244
+
245
+ def _process_pattern_group(
246
+ patterns: list[StoredPattern],
247
+ config: StringlyTypedConfig,
248
+ rule_id: str,
249
+ violations: list[Violation],
250
+ covered_variables: set[str],
251
+ ) -> None:
252
+ """Process a group of patterns with the same hash."""
253
+ if _should_skip_patterns(patterns, config):
254
+ return
255
+ violations.extend(_build_violation(p, patterns, rule_id) for p in patterns)
256
+ for pattern in patterns:
257
+ if pattern.variable_name:
258
+ covered_variables.add(pattern.variable_name)
259
+
260
+
261
+ # --- Helper functions for function call processing ---
262
+
263
+
264
+ def _is_valid_function(name: str, idx: int, vals: set[str], config: StringlyTypedConfig) -> bool:
265
+ """Check if a function passes all validity filters."""
266
+ if _is_allowed_value_set(vals, config):
267
+ return False
268
+ if _has_spaces(vals):
269
+ return False
270
+ return context_filter.should_include(name, idx, vals)
271
+
272
+
273
+ def _get_valid_functions(
274
+ storage: StringlyTypedStorage,
275
+ config: StringlyTypedConfig,
276
+ ) -> list[tuple[str, int, set[str]]]:
277
+ """Get functions that pass all filters."""
278
+ min_files = config.min_occurrences if config.require_cross_file else 1
279
+ limited_funcs = storage.get_limited_value_functions(
280
+ min_values=config.min_values_for_enum,
281
+ max_values=config.max_values_for_enum,
282
+ min_files=min_files,
283
+ )
284
+ return [(n, i, v) for n, i, v in limited_funcs if _is_valid_function(n, i, v, config)]
285
+
286
+
287
+ def _build_call_violations(
288
+ valid_funcs: list[tuple[str, int, set[str]]],
289
+ storage: StringlyTypedStorage,
290
+ ) -> list[Violation]:
291
+ """Build violations for valid function patterns."""
292
+ violations: list[Violation] = []
293
+ for function_name, param_index, unique_values in valid_funcs:
294
+ calls = storage.get_calls_by_function(function_name, param_index)
295
+ violations.extend(build_function_call_violations(calls, unique_values))
296
+ return violations
297
+
298
+
299
+ # --- Helper functions for comparison processing ---
300
+
301
+
302
+ def _get_variables_to_check(
303
+ storage: StringlyTypedStorage,
304
+ config: StringlyTypedConfig,
305
+ ) -> list[tuple[str, set[str]]]:
306
+ """Get variables with multiple values that should be checked."""
307
+ min_files = config.min_occurrences if config.require_cross_file else 1
308
+ return storage.get_variables_with_multiple_values(
309
+ min_values=config.min_values_for_enum,
310
+ min_files=min_files,
311
+ )
312
+
313
+
314
+ def _process_variable( # pylint: disable=too-many-arguments,too-many-positional-arguments
315
+ variable_name: str,
316
+ unique_values: set[str],
317
+ storage: StringlyTypedStorage,
318
+ config: StringlyTypedConfig,
319
+ covered_variables: set[str],
320
+ violations: list[Violation],
321
+ ) -> None:
322
+ """Process comparisons for a single variable."""
323
+ if variable_name in covered_variables:
324
+ return
325
+ if _should_skip_variable(variable_name):
326
+ return
327
+ if _should_skip_comparison(unique_values, config):
328
+ return
329
+ comparisons = storage.get_comparisons_by_variable(variable_name)
330
+ violations.extend(
331
+ _build_comparison_violation(c, comparisons, unique_values) for c in comparisons
332
+ )
333
+
334
+
335
+ # --- ViolationGenerator class ---
336
+
337
+
338
+ class ViolationGenerator:
339
+ """Generates violations from cross-file stringly-typed patterns."""
340
+
341
+ def __init__(self) -> None:
342
+ """Initialize with helper filters."""
343
+ self._ignore_checker = IgnoreChecker()
344
+
345
+ def generate_violations(
346
+ self,
347
+ storage: StringlyTypedStorage,
348
+ rule_id: str,
349
+ config: StringlyTypedConfig,
350
+ ) -> list[Violation]:
351
+ """Generate violations from storage.
352
+
353
+ Args:
354
+ storage: Pattern storage instance
355
+ rule_id: Rule identifier for violations
356
+ config: Stringly-typed configuration with thresholds
357
+
358
+ Returns:
359
+ List of violations for patterns appearing in multiple files
360
+ """
361
+ violations: list[Violation] = []
362
+ pattern_violations, covered_vars = self._generate_pattern_violations(
363
+ storage, rule_id, config
364
+ )
365
+ violations.extend(pattern_violations)
366
+ violations.extend(self._generate_function_call_violations(storage, config))
367
+ violations.extend(self._generate_comparison_violations(storage, config, covered_vars))
368
+
369
+ # Apply path-based ignore patterns from config
370
+ violations = _filter_by_ignore(violations, config.ignore)
371
+
372
+ # Apply inline ignore directives via IgnoreChecker
373
+ violations = self._ignore_checker.filter_violations(violations)
374
+
375
+ return violations
376
+
377
+ def _generate_pattern_violations(
378
+ self,
379
+ storage: StringlyTypedStorage,
380
+ rule_id: str,
381
+ config: StringlyTypedConfig,
382
+ ) -> tuple[list[Violation], set[str]]:
383
+ """Generate violations for duplicate validation patterns."""
384
+ duplicate_hashes = storage.get_duplicate_hashes(min_files=config.min_occurrences)
385
+ violations: list[Violation] = []
386
+ covered_variables: set[str] = set()
387
+
388
+ for hash_value in duplicate_hashes:
389
+ patterns = storage.get_patterns_by_hash(hash_value)
390
+ _process_pattern_group(patterns, config, rule_id, violations, covered_variables)
391
+
392
+ return violations, covered_variables
393
+
394
+ def _generate_function_call_violations(
395
+ self,
396
+ storage: StringlyTypedStorage,
397
+ config: StringlyTypedConfig,
398
+ ) -> list[Violation]:
399
+ """Generate violations for function call patterns."""
400
+ valid_funcs = _get_valid_functions(storage, config)
401
+ return _build_call_violations(valid_funcs, storage)
402
+
403
+ def _generate_comparison_violations(
404
+ self,
405
+ storage: StringlyTypedStorage,
406
+ config: StringlyTypedConfig,
407
+ covered_variables: set[str] | None = None,
408
+ ) -> list[Violation]:
409
+ """Generate violations for scattered string comparisons."""
410
+ covered_variables = covered_variables or set()
411
+ variables = _get_variables_to_check(storage, config)
412
+
413
+ violations: list[Violation] = []
414
+ for variable_name, unique_values in variables:
415
+ _process_variable(
416
+ variable_name, unique_values, storage, config, covered_variables, violations
417
+ )
418
+
419
+ return violations