thailint 0.12.0__py3-none-any.whl → 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. src/analyzers/__init__.py +4 -3
  2. src/analyzers/ast_utils.py +54 -0
  3. src/analyzers/typescript_base.py +4 -0
  4. src/cli/__init__.py +3 -0
  5. src/cli/config.py +12 -12
  6. src/cli/config_merge.py +241 -0
  7. src/cli/linters/__init__.py +3 -0
  8. src/cli/linters/code_patterns.py +113 -5
  9. src/cli/linters/code_smells.py +4 -0
  10. src/cli/linters/documentation.py +3 -0
  11. src/cli/linters/structure.py +3 -0
  12. src/cli/linters/structure_quality.py +3 -0
  13. src/cli_main.py +3 -0
  14. src/config.py +2 -1
  15. src/core/base.py +3 -2
  16. src/core/cli_utils.py +3 -1
  17. src/core/config_parser.py +5 -2
  18. src/core/constants.py +54 -0
  19. src/core/linter_utils.py +4 -0
  20. src/core/rule_discovery.py +5 -1
  21. src/core/violation_builder.py +3 -0
  22. src/linter_config/directive_markers.py +109 -0
  23. src/linter_config/ignore.py +225 -383
  24. src/linter_config/pattern_utils.py +65 -0
  25. src/linter_config/rule_matcher.py +89 -0
  26. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  27. src/linters/collection_pipeline/ast_utils.py +40 -0
  28. src/linters/collection_pipeline/config.py +12 -0
  29. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  30. src/linters/collection_pipeline/detector.py +262 -32
  31. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  32. src/linters/collection_pipeline/linter.py +18 -35
  33. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  34. src/linters/dry/base_token_analyzer.py +16 -9
  35. src/linters/dry/block_filter.py +7 -4
  36. src/linters/dry/cache.py +7 -2
  37. src/linters/dry/config.py +7 -1
  38. src/linters/dry/constant_matcher.py +34 -25
  39. src/linters/dry/file_analyzer.py +4 -2
  40. src/linters/dry/inline_ignore.py +7 -16
  41. src/linters/dry/linter.py +48 -25
  42. src/linters/dry/python_analyzer.py +18 -10
  43. src/linters/dry/python_constant_extractor.py +51 -52
  44. src/linters/dry/single_statement_detector.py +14 -12
  45. src/linters/dry/token_hasher.py +115 -115
  46. src/linters/dry/typescript_analyzer.py +11 -6
  47. src/linters/dry/typescript_constant_extractor.py +4 -0
  48. src/linters/dry/typescript_statement_detector.py +208 -208
  49. src/linters/dry/typescript_value_extractor.py +3 -0
  50. src/linters/dry/violation_filter.py +1 -4
  51. src/linters/dry/violation_generator.py +1 -4
  52. src/linters/file_header/atemporal_detector.py +4 -0
  53. src/linters/file_header/base_parser.py +4 -0
  54. src/linters/file_header/bash_parser.py +4 -0
  55. src/linters/file_header/field_validator.py +5 -8
  56. src/linters/file_header/linter.py +19 -12
  57. src/linters/file_header/markdown_parser.py +6 -0
  58. src/linters/file_placement/config_loader.py +3 -1
  59. src/linters/file_placement/linter.py +22 -8
  60. src/linters/file_placement/pattern_matcher.py +21 -4
  61. src/linters/file_placement/pattern_validator.py +21 -7
  62. src/linters/file_placement/rule_checker.py +2 -2
  63. src/linters/lazy_ignores/__init__.py +43 -0
  64. src/linters/lazy_ignores/config.py +66 -0
  65. src/linters/lazy_ignores/directive_utils.py +121 -0
  66. src/linters/lazy_ignores/header_parser.py +177 -0
  67. src/linters/lazy_ignores/linter.py +158 -0
  68. src/linters/lazy_ignores/matcher.py +135 -0
  69. src/linters/lazy_ignores/python_analyzer.py +201 -0
  70. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  71. src/linters/lazy_ignores/skip_detector.py +298 -0
  72. src/linters/lazy_ignores/types.py +67 -0
  73. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  74. src/linters/lazy_ignores/violation_builder.py +131 -0
  75. src/linters/lbyl/__init__.py +29 -0
  76. src/linters/lbyl/config.py +63 -0
  77. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  78. src/linters/lbyl/pattern_detectors/base.py +46 -0
  79. src/linters/magic_numbers/context_analyzer.py +227 -229
  80. src/linters/magic_numbers/linter.py +20 -15
  81. src/linters/magic_numbers/python_analyzer.py +4 -16
  82. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  83. src/linters/method_property/config.py +4 -0
  84. src/linters/method_property/linter.py +5 -4
  85. src/linters/method_property/python_analyzer.py +5 -4
  86. src/linters/method_property/violation_builder.py +3 -0
  87. src/linters/nesting/typescript_analyzer.py +6 -12
  88. src/linters/nesting/typescript_function_extractor.py +0 -4
  89. src/linters/print_statements/linter.py +6 -4
  90. src/linters/print_statements/python_analyzer.py +85 -81
  91. src/linters/print_statements/typescript_analyzer.py +6 -15
  92. src/linters/srp/heuristics.py +4 -4
  93. src/linters/srp/linter.py +12 -12
  94. src/linters/srp/violation_builder.py +0 -4
  95. src/linters/stateless_class/linter.py +30 -36
  96. src/linters/stateless_class/python_analyzer.py +11 -20
  97. src/linters/stringly_typed/config.py +4 -5
  98. src/linters/stringly_typed/context_filter.py +410 -410
  99. src/linters/stringly_typed/function_call_violation_builder.py +93 -95
  100. src/linters/stringly_typed/linter.py +48 -16
  101. src/linters/stringly_typed/python/analyzer.py +5 -1
  102. src/linters/stringly_typed/python/call_tracker.py +8 -5
  103. src/linters/stringly_typed/python/comparison_tracker.py +10 -5
  104. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  105. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  106. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  107. src/linters/stringly_typed/python/validation_detector.py +3 -0
  108. src/linters/stringly_typed/storage.py +14 -14
  109. src/linters/stringly_typed/typescript/call_tracker.py +9 -3
  110. src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
  111. src/linters/stringly_typed/violation_generator.py +288 -259
  112. src/orchestrator/core.py +13 -4
  113. src/templates/thailint_config_template.yaml +166 -0
  114. src/utils/project_root.py +3 -0
  115. thailint-0.13.0.dist-info/METADATA +184 -0
  116. thailint-0.13.0.dist-info/RECORD +189 -0
  117. thailint-0.12.0.dist-info/METADATA +0 -1667
  118. thailint-0.12.0.dist-info/RECORD +0 -164
  119. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
  120. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
  121. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -14,26 +14,33 @@ Overview: Handles violation generation for stringly-typed patterns that appear a
14
14
  Separates violation generation logic from main linter rule to maintain SRP compliance.
15
15
 
16
16
  Dependencies: StringlyTypedStorage, StoredPattern, StoredComparison, StringlyTypedConfig,
17
- Violation, Severity, FunctionCallViolationBuilder, IgnoreChecker
17
+ Violation, Severity, build_function_call_violations, IgnoreChecker
18
18
 
19
- Exports: ViolationGenerator class
19
+ Exports: ViolationGenerator class, pure helper functions for message building
20
20
 
21
21
  Interfaces: ViolationGenerator.generate_violations(storage, rule_id, config) -> list[Violation]
22
22
 
23
23
  Implementation: Queries storage, validates pattern thresholds, builds violations with
24
24
  cross-file references, delegates function call violations to builder, generates
25
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
26
31
  """
27
32
 
28
33
  from src.core.types import Severity, Violation
29
34
 
35
+ from . import context_filter
30
36
  from .config import StringlyTypedConfig
31
- from .context_filter import FunctionCallFilter
32
- from .function_call_violation_builder import FunctionCallViolationBuilder
37
+ from .function_call_violation_builder import build_function_call_violations
33
38
  from .ignore_checker import IgnoreChecker
34
39
  from .ignore_utils import is_ignored
35
40
  from .storage import StoredComparison, StoredPattern, StringlyTypedStorage
36
41
 
42
+ # --- Pure helper functions for filtering ---
43
+
37
44
 
38
45
  def _filter_by_ignore(violations: list[Violation], ignore: list[str]) -> list[Violation]:
39
46
  """Filter violations by ignore patterns."""
@@ -47,13 +54,278 @@ def _is_allowed_value_set(values: set[str], config: StringlyTypedConfig) -> bool
47
54
  return any(values == set(allowed) for allowed in config.allowed_string_sets)
48
55
 
49
56
 
50
- class ViolationGenerator: # thailint: ignore srp
57
+ def _is_enum_candidate(pattern: StoredPattern, config: StringlyTypedConfig) -> bool:
58
+ """Check if pattern's value count is within enum range."""
59
+ value_count = len(pattern.string_values)
60
+ return config.min_values_for_enum <= value_count <= config.max_values_for_enum
61
+
62
+
63
+ def _is_pattern_allowed(pattern: StoredPattern, config: StringlyTypedConfig) -> bool:
64
+ """Check if pattern's string set is in allowed list."""
65
+ return _is_allowed_value_set(set(pattern.string_values), config)
66
+
67
+
68
+ def _should_skip_patterns(patterns: list[StoredPattern], config: StringlyTypedConfig) -> bool:
69
+ """Check if pattern group should be skipped based on config."""
70
+ if not patterns:
71
+ return True
72
+ first = patterns[0]
73
+ if not _is_enum_candidate(first, config):
74
+ return True
75
+ if _is_pattern_allowed(first, config):
76
+ return True
77
+ # Skip if all values match excluded patterns (numeric strings, etc.)
78
+ if context_filter.are_all_values_excluded(set(first.string_values)):
79
+ return True
80
+ return False
81
+
82
+
83
+ def _should_skip_comparison(unique_values: set[str], config: StringlyTypedConfig) -> bool:
84
+ """Check if a comparison pattern should be skipped based on config."""
85
+ if len(unique_values) > config.max_values_for_enum:
86
+ return True
87
+ if _is_allowed_value_set(unique_values, config):
88
+ return True
89
+ if context_filter.are_all_values_excluded(unique_values):
90
+ return True
91
+ return False
92
+
93
+
94
+ # Variable suffixes that indicate false positive comparisons
95
+ _EXCLUDED_VARIABLE_SUFFIXES: tuple[str, ...] = (
96
+ ".value", # Enum value access
97
+ ".method", # HTTP method
98
+ ".type", # Tree-sitter node types
99
+ )
100
+
101
+
102
+ def _should_skip_variable(variable_name: str) -> bool:
103
+ """Check if a variable name indicates a false positive comparison."""
104
+ # Check excluded suffixes
105
+ if any(variable_name.endswith(s) for s in _EXCLUDED_VARIABLE_SUFFIXES):
106
+ return True
107
+ # Test assertion patterns (underscore prefix is common in comprehensions/lambdas)
108
+ return variable_name.startswith("_.")
109
+
110
+
111
+ # --- Pure helper functions for message building ---
112
+
113
+
114
+ def _build_cross_references(pattern: StoredPattern, all_patterns: list[StoredPattern]) -> str:
115
+ """Build cross-reference string for other files."""
116
+ refs = [
117
+ f"{other.file_path.name}:{other.line_number}"
118
+ for other in all_patterns
119
+ if other.file_path != pattern.file_path
120
+ ]
121
+ return ", ".join(refs)
122
+
123
+
124
+ def _build_message(pattern: StoredPattern, all_patterns: list[StoredPattern]) -> str:
125
+ """Build violation message with cross-file references."""
126
+ file_count = len({p.file_path for p in all_patterns})
127
+ values_str = ", ".join(f"'{v}'" for v in sorted(pattern.string_values))
128
+ other_refs = _build_cross_references(pattern, all_patterns)
129
+
130
+ message = f"Stringly-typed pattern with values [{values_str}] appears in {file_count} files."
131
+ if other_refs:
132
+ message += f" Also found in: {other_refs}."
133
+
134
+ return message
135
+
136
+
137
+ def _build_suggestion(pattern: StoredPattern) -> str:
138
+ """Build fix suggestion for the pattern."""
139
+ values_count = len(pattern.string_values)
140
+ var_info = f" for '{pattern.variable_name}'" if pattern.variable_name else ""
141
+
142
+ return (
143
+ f"Consider defining an enum or type union{var_info} with the "
144
+ f"{values_count} possible values instead of using string literals."
145
+ )
146
+
147
+
148
+ def _build_comparison_cross_references(
149
+ comparison: StoredComparison,
150
+ all_comparisons: list[StoredComparison],
151
+ ) -> str:
152
+ """Build cross-reference string for other comparison locations."""
153
+ refs = [
154
+ f"{other.file_path.name}:{other.line_number}"
155
+ for other in all_comparisons
156
+ if other.file_path != comparison.file_path
157
+ ]
158
+ return ", ".join(refs)
159
+
160
+
161
+ def _build_comparison_message(
162
+ comparison: StoredComparison,
163
+ all_comparisons: list[StoredComparison],
164
+ unique_values: set[str],
165
+ ) -> str:
166
+ """Build violation message for scattered comparison."""
167
+ file_count = len({c.file_path for c in all_comparisons})
168
+ values_str = ", ".join(f"'{v}'" for v in sorted(unique_values))
169
+ other_refs = _build_comparison_cross_references(comparison, all_comparisons)
170
+
171
+ message = (
172
+ f"Variable '{comparison.variable_name}' is compared to {len(unique_values)} "
173
+ f"different string values [{values_str}] across {file_count} file(s)."
174
+ )
175
+ if other_refs:
176
+ message += f" Also compared in: {other_refs}."
177
+
178
+ return message
179
+
180
+
181
+ def _build_comparison_suggestion(comparison: StoredComparison, unique_values: set[str]) -> str:
182
+ """Build fix suggestion for scattered comparison."""
183
+ return (
184
+ f"Consider defining an enum for '{comparison.variable_name}' with the "
185
+ f"{len(unique_values)} possible values instead of using string literals "
186
+ f"in scattered comparisons."
187
+ )
188
+
189
+
190
+ # --- Pure helper function for violation building ---
191
+
192
+
193
+ def _build_violation(
194
+ pattern: StoredPattern, all_patterns: list[StoredPattern], rule_id: str
195
+ ) -> Violation:
196
+ """Build a violation for a pattern with cross-references."""
197
+ message = _build_message(pattern, all_patterns)
198
+ suggestion = _build_suggestion(pattern)
199
+
200
+ return Violation(
201
+ rule_id=rule_id,
202
+ file_path=str(pattern.file_path),
203
+ line=pattern.line_number,
204
+ column=pattern.column,
205
+ message=message,
206
+ severity=Severity.ERROR,
207
+ suggestion=suggestion,
208
+ )
209
+
210
+
211
+ def _build_comparison_violation(
212
+ comparison: StoredComparison,
213
+ all_comparisons: list[StoredComparison],
214
+ unique_values: set[str],
215
+ ) -> Violation:
216
+ """Build a violation for a scattered string comparison."""
217
+ message = _build_comparison_message(comparison, all_comparisons, unique_values)
218
+ suggestion = _build_comparison_suggestion(comparison, unique_values)
219
+
220
+ return Violation(
221
+ rule_id="stringly-typed.scattered-comparison",
222
+ file_path=str(comparison.file_path),
223
+ line=comparison.line_number,
224
+ column=comparison.column,
225
+ message=message,
226
+ severity=Severity.ERROR,
227
+ suggestion=suggestion,
228
+ )
229
+
230
+
231
+ # --- Helper functions for pattern processing ---
232
+
233
+
234
+ def _process_pattern_group(
235
+ patterns: list[StoredPattern],
236
+ config: StringlyTypedConfig,
237
+ rule_id: str,
238
+ violations: list[Violation],
239
+ covered_variables: set[str],
240
+ ) -> None:
241
+ """Process a group of patterns with the same hash."""
242
+ if _should_skip_patterns(patterns, config):
243
+ return
244
+ violations.extend(_build_violation(p, patterns, rule_id) for p in patterns)
245
+ for pattern in patterns:
246
+ if pattern.variable_name:
247
+ covered_variables.add(pattern.variable_name)
248
+
249
+
250
+ # --- Helper functions for function call processing ---
251
+
252
+
253
+ def _get_valid_functions(
254
+ storage: StringlyTypedStorage,
255
+ config: StringlyTypedConfig,
256
+ ) -> list[tuple[str, int, set[str]]]:
257
+ """Get functions that pass all filters."""
258
+ min_files = config.min_occurrences if config.require_cross_file else 1
259
+ limited_funcs = storage.get_limited_value_functions(
260
+ min_values=config.min_values_for_enum,
261
+ max_values=config.max_values_for_enum,
262
+ min_files=min_files,
263
+ )
264
+
265
+ return [
266
+ (name, idx, vals)
267
+ for name, idx, vals in limited_funcs
268
+ if not _is_allowed_value_set(vals, config)
269
+ and context_filter.should_include(name, idx, vals)
270
+ ]
271
+
272
+
273
+ def _build_call_violations(
274
+ valid_funcs: list[tuple[str, int, set[str]]],
275
+ storage: StringlyTypedStorage,
276
+ ) -> list[Violation]:
277
+ """Build violations for valid function patterns."""
278
+ violations: list[Violation] = []
279
+ for function_name, param_index, unique_values in valid_funcs:
280
+ calls = storage.get_calls_by_function(function_name, param_index)
281
+ violations.extend(build_function_call_violations(calls, unique_values))
282
+ return violations
283
+
284
+
285
+ # --- Helper functions for comparison processing ---
286
+
287
+
288
+ def _get_variables_to_check(
289
+ storage: StringlyTypedStorage,
290
+ config: StringlyTypedConfig,
291
+ ) -> list[tuple[str, set[str]]]:
292
+ """Get variables with multiple values that should be checked."""
293
+ min_files = config.min_occurrences if config.require_cross_file else 1
294
+ return storage.get_variables_with_multiple_values(
295
+ min_values=config.min_values_for_enum,
296
+ min_files=min_files,
297
+ )
298
+
299
+
300
+ def _process_variable( # pylint: disable=too-many-arguments,too-many-positional-arguments
301
+ variable_name: str,
302
+ unique_values: set[str],
303
+ storage: StringlyTypedStorage,
304
+ config: StringlyTypedConfig,
305
+ covered_variables: set[str],
306
+ violations: list[Violation],
307
+ ) -> None:
308
+ """Process comparisons for a single variable."""
309
+ if variable_name in covered_variables:
310
+ return
311
+ if _should_skip_variable(variable_name):
312
+ return
313
+ if _should_skip_comparison(unique_values, config):
314
+ return
315
+ comparisons = storage.get_comparisons_by_variable(variable_name)
316
+ violations.extend(
317
+ _build_comparison_violation(c, comparisons, unique_values) for c in comparisons
318
+ )
319
+
320
+
321
+ # --- ViolationGenerator class ---
322
+
323
+
324
+ class ViolationGenerator:
51
325
  """Generates violations from cross-file stringly-typed patterns."""
52
326
 
53
327
  def __init__(self) -> None:
54
- """Initialize with helper builders and filters."""
55
- self._call_builder = FunctionCallViolationBuilder()
56
- self._call_filter = FunctionCallFilter()
328
+ """Initialize with helper filters."""
57
329
  self._ignore_checker = IgnoreChecker()
58
330
 
59
331
  def generate_violations(
@@ -83,7 +355,7 @@ class ViolationGenerator: # thailint: ignore srp
83
355
  # Apply path-based ignore patterns from config
84
356
  violations = _filter_by_ignore(violations, config.ignore)
85
357
 
86
- # Apply inline ignore directives (# thailint: ignore[stringly-typed])
358
+ # Apply inline ignore directives via IgnoreChecker
87
359
  violations = self._ignore_checker.filter_violations(violations)
88
360
 
89
361
  return violations
@@ -94,139 +366,25 @@ class ViolationGenerator: # thailint: ignore srp
94
366
  rule_id: str,
95
367
  config: StringlyTypedConfig,
96
368
  ) -> tuple[list[Violation], set[str]]:
97
- """Generate violations for duplicate validation patterns.
98
-
99
- Returns:
100
- Tuple of (violations list, set of variable names covered by these violations)
101
- """
369
+ """Generate violations for duplicate validation patterns."""
102
370
  duplicate_hashes = storage.get_duplicate_hashes(min_files=config.min_occurrences)
103
371
  violations: list[Violation] = []
104
372
  covered_variables: set[str] = set()
105
373
 
106
374
  for hash_value in duplicate_hashes:
107
375
  patterns = storage.get_patterns_by_hash(hash_value)
108
- self._process_pattern_group(patterns, config, rule_id, violations, covered_variables)
376
+ _process_pattern_group(patterns, config, rule_id, violations, covered_variables)
109
377
 
110
378
  return violations, covered_variables
111
379
 
112
- def _process_pattern_group( # pylint: disable=too-many-arguments,too-many-positional-arguments
113
- self,
114
- patterns: list[StoredPattern],
115
- config: StringlyTypedConfig,
116
- rule_id: str,
117
- violations: list[Violation],
118
- covered_variables: set[str],
119
- ) -> None:
120
- """Process a group of patterns with the same hash."""
121
- if self._should_skip_patterns(patterns, config):
122
- return
123
- violations.extend(self._build_violation(p, patterns, rule_id) for p in patterns)
124
- # Track variable names to avoid duplicate comparison violations
125
- for pattern in patterns:
126
- if pattern.variable_name:
127
- covered_variables.add(pattern.variable_name)
128
-
129
380
  def _generate_function_call_violations(
130
381
  self,
131
382
  storage: StringlyTypedStorage,
132
383
  config: StringlyTypedConfig,
133
384
  ) -> list[Violation]:
134
385
  """Generate violations for function call patterns."""
135
- min_files = config.min_occurrences if config.require_cross_file else 1
136
- limited_funcs = storage.get_limited_value_functions(
137
- min_values=config.min_values_for_enum,
138
- max_values=config.max_values_for_enum,
139
- min_files=min_files,
140
- )
141
-
142
- violations: list[Violation] = []
143
- for function_name, param_index, unique_values in limited_funcs:
144
- if _is_allowed_value_set(unique_values, config):
145
- continue
146
- # Apply context-aware filtering to reduce false positives
147
- if not self._call_filter.should_include(function_name, param_index, unique_values):
148
- continue
149
- calls = storage.get_calls_by_function(function_name, param_index)
150
- violations.extend(self._call_builder.build_violations(calls, unique_values))
151
-
152
- return violations
153
-
154
- def _should_skip_patterns(
155
- self, patterns: list[StoredPattern], config: StringlyTypedConfig
156
- ) -> bool:
157
- """Check if pattern group should be skipped based on config."""
158
- if not patterns:
159
- return True
160
- first = patterns[0]
161
- if not self._is_enum_candidate(first, config):
162
- return True
163
- if self._is_pattern_allowed(first, config):
164
- return True
165
- # Skip if all values match excluded patterns (numeric strings, etc.)
166
- if self._call_filter.are_all_values_excluded(set(first.string_values)):
167
- return True
168
- return False
169
-
170
- def _is_enum_candidate(self, pattern: StoredPattern, config: StringlyTypedConfig) -> bool:
171
- """Check if pattern's value count is within enum range."""
172
- value_count = len(pattern.string_values)
173
- return config.min_values_for_enum <= value_count <= config.max_values_for_enum
174
-
175
- def _is_pattern_allowed(self, pattern: StoredPattern, config: StringlyTypedConfig) -> bool:
176
- """Check if pattern's string set is in allowed list."""
177
- return _is_allowed_value_set(set(pattern.string_values), config)
178
-
179
- def _build_violation(
180
- self, pattern: StoredPattern, all_patterns: list[StoredPattern], rule_id: str
181
- ) -> Violation:
182
- """Build a violation for a pattern with cross-references."""
183
- message = self._build_message(pattern, all_patterns)
184
- suggestion = self._build_suggestion(pattern)
185
-
186
- return Violation(
187
- rule_id=rule_id,
188
- file_path=str(pattern.file_path),
189
- line=pattern.line_number,
190
- column=pattern.column,
191
- message=message,
192
- severity=Severity.ERROR,
193
- suggestion=suggestion,
194
- )
195
-
196
- def _build_message(self, pattern: StoredPattern, all_patterns: list[StoredPattern]) -> str:
197
- """Build violation message with cross-file references."""
198
- file_count = len({p.file_path for p in all_patterns})
199
- values_str = ", ".join(f"'{v}'" for v in sorted(pattern.string_values))
200
- other_refs = self._build_cross_references(pattern, all_patterns)
201
-
202
- message = (
203
- f"Stringly-typed pattern with values [{values_str}] appears in {file_count} files."
204
- )
205
- if other_refs:
206
- message += f" Also found in: {other_refs}."
207
-
208
- return message
209
-
210
- def _build_cross_references(
211
- self, pattern: StoredPattern, all_patterns: list[StoredPattern]
212
- ) -> str:
213
- """Build cross-reference string for other files."""
214
- refs = [
215
- f"{other.file_path.name}:{other.line_number}"
216
- for other in all_patterns
217
- if other.file_path != pattern.file_path
218
- ]
219
- return ", ".join(refs)
220
-
221
- def _build_suggestion(self, pattern: StoredPattern) -> str:
222
- """Build fix suggestion for the pattern."""
223
- values_count = len(pattern.string_values)
224
- var_info = f" for '{pattern.variable_name}'" if pattern.variable_name else ""
225
-
226
- return (
227
- f"Consider defining an enum or type union{var_info} with the "
228
- f"{values_count} possible values instead of using string literals."
229
- )
386
+ valid_funcs = _get_valid_functions(storage, config)
387
+ return _build_call_violations(valid_funcs, storage)
230
388
 
231
389
  def _generate_comparison_violations(
232
390
  self,
@@ -234,143 +392,14 @@ class ViolationGenerator: # thailint: ignore srp
234
392
  config: StringlyTypedConfig,
235
393
  covered_variables: set[str] | None = None,
236
394
  ) -> list[Violation]:
237
- """Generate violations for scattered string comparisons.
238
-
239
- Finds variables that are compared to multiple unique string values across
240
- files (e.g., `if env == "production"` in one file and `if env == "staging"`
241
- in another), suggesting they should use enums instead.
242
-
243
- Args:
244
- storage: Pattern storage instance
245
- config: Stringly-typed configuration
246
- covered_variables: Variable names already flagged by pattern violations (to deduplicate)
247
- """
395
+ """Generate violations for scattered string comparisons."""
248
396
  covered_variables = covered_variables or set()
249
- min_files = config.min_occurrences if config.require_cross_file else 1
250
- variables = storage.get_variables_with_multiple_values(
251
- min_values=config.min_values_for_enum,
252
- min_files=min_files,
253
- )
397
+ variables = _get_variables_to_check(storage, config)
254
398
 
255
399
  violations: list[Violation] = []
256
400
  for variable_name, unique_values in variables:
257
- self._process_variable_comparisons(
401
+ _process_variable(
258
402
  variable_name, unique_values, storage, config, covered_variables, violations
259
403
  )
260
404
 
261
405
  return violations
262
-
263
- def _process_variable_comparisons( # pylint: disable=too-many-arguments,too-many-positional-arguments
264
- self,
265
- variable_name: str,
266
- unique_values: set[str],
267
- storage: StringlyTypedStorage,
268
- config: StringlyTypedConfig,
269
- covered_variables: set[str],
270
- violations: list[Violation],
271
- ) -> None:
272
- """Process comparisons for a single variable."""
273
- # Skip if already covered by equality chain or validation pattern
274
- if variable_name in covered_variables:
275
- return
276
- # Skip false positive variable patterns (.value, .method, etc.)
277
- if self._should_skip_variable(variable_name):
278
- return
279
- if self._should_skip_comparison(unique_values, config):
280
- return
281
- comparisons = storage.get_comparisons_by_variable(variable_name)
282
- violations.extend(
283
- self._build_comparison_violation(c, comparisons, unique_values) for c in comparisons
284
- )
285
-
286
- def _should_skip_comparison(self, unique_values: set[str], config: StringlyTypedConfig) -> bool:
287
- """Check if a comparison pattern should be skipped based on config."""
288
- if len(unique_values) > config.max_values_for_enum:
289
- return True
290
- if _is_allowed_value_set(unique_values, config):
291
- return True
292
- if self._call_filter.are_all_values_excluded(unique_values):
293
- return True
294
- return False
295
-
296
- def _should_skip_variable(self, variable_name: str) -> bool:
297
- """Check if a variable name indicates a false positive comparison.
298
-
299
- Excludes:
300
- - Variables ending with .value (enum value access)
301
- - HTTP method variables (request.method, etc.)
302
- - Variables that are likely test fixtures (underscore prefix patterns)
303
- """
304
- # Enum value access - already using an enum
305
- if variable_name.endswith(".value"):
306
- return True
307
- # HTTP method - standard protocol strings
308
- if variable_name.endswith(".method"):
309
- return True
310
- # Test assertion patterns (underscore prefix is common in comprehensions/lambdas)
311
- if variable_name.startswith("_."):
312
- return True
313
- return False
314
-
315
- def _build_comparison_violation(
316
- self,
317
- comparison: StoredComparison,
318
- all_comparisons: list[StoredComparison],
319
- unique_values: set[str],
320
- ) -> Violation:
321
- """Build a violation for a scattered string comparison."""
322
- message = self._build_comparison_message(comparison, all_comparisons, unique_values)
323
- suggestion = self._build_comparison_suggestion(comparison, unique_values)
324
-
325
- return Violation(
326
- rule_id="stringly-typed.scattered-comparison",
327
- file_path=str(comparison.file_path),
328
- line=comparison.line_number,
329
- column=comparison.column,
330
- message=message,
331
- severity=Severity.ERROR,
332
- suggestion=suggestion,
333
- )
334
-
335
- def _build_comparison_message(
336
- self,
337
- comparison: StoredComparison,
338
- all_comparisons: list[StoredComparison],
339
- unique_values: set[str],
340
- ) -> str:
341
- """Build violation message for scattered comparison."""
342
- file_count = len({c.file_path for c in all_comparisons})
343
- values_str = ", ".join(f"'{v}'" for v in sorted(unique_values))
344
- other_refs = self._build_comparison_cross_references(comparison, all_comparisons)
345
-
346
- message = (
347
- f"Variable '{comparison.variable_name}' is compared to {len(unique_values)} "
348
- f"different string values [{values_str}] across {file_count} file(s)."
349
- )
350
- if other_refs:
351
- message += f" Also compared in: {other_refs}."
352
-
353
- return message
354
-
355
- def _build_comparison_cross_references(
356
- self,
357
- comparison: StoredComparison,
358
- all_comparisons: list[StoredComparison],
359
- ) -> str:
360
- """Build cross-reference string for other comparison locations."""
361
- refs = [
362
- f"{other.file_path.name}:{other.line_number}"
363
- for other in all_comparisons
364
- if other.file_path != comparison.file_path
365
- ]
366
- return ", ".join(refs)
367
-
368
- def _build_comparison_suggestion(
369
- self, comparison: StoredComparison, unique_values: set[str]
370
- ) -> str:
371
- """Build fix suggestion for scattered comparison."""
372
- return (
373
- f"Consider defining an enum for '{comparison.variable_name}' with the "
374
- f"{len(unique_values)} possible values instead of using string literals "
375
- f"in scattered comparisons."
376
- )
src/orchestrator/core.py CHANGED
@@ -29,10 +29,15 @@ Implementation: Directory glob pattern matching for traversal (** for recursive,
29
29
  ignore pattern checking before file processing, dynamic context creation per file,
30
30
  rule filtering by applicability, violation collection and aggregation across files,
31
31
  ProcessPoolExecutor for parallel file processing
32
+
33
+ Suppressions:
34
+ - srp: Orchestrator class coordinates multiple subsystems by design (registry, config, ignore,
35
+ language detection). Splitting would fragment the core linting workflow.
32
36
  """
33
37
 
34
38
  from __future__ import annotations
35
39
 
40
+ import logging
36
41
  import multiprocessing
37
42
  import os
38
43
  from concurrent.futures import Future, ProcessPoolExecutor, as_completed
@@ -46,6 +51,8 @@ from src.linter_config.loader import LinterConfigLoader
46
51
 
47
52
  from .language_detector import detect_language
48
53
 
54
+ logger = logging.getLogger(__name__)
55
+
49
56
  # Default max workers for parallel processing (capped to avoid resource contention)
50
57
  DEFAULT_MAX_WORKERS = 8
51
58
 
@@ -163,7 +170,8 @@ def _lint_file_worker(args: tuple[Path, Path, dict]) -> list[dict]:
163
170
  violations = orchestrator.lint_file(file_path)
164
171
  # Convert to dicts for pickling
165
172
  return [v.to_dict() for v in violations]
166
- except Exception: # nosec B112 - defensive; don't crash on worker errors
173
+ except Exception:
174
+ logger.exception("Worker error processing file: %s", file_path)
167
175
  return []
168
176
 
169
177
 
@@ -334,8 +342,8 @@ class Orchestrator: # thailint: ignore[srp]
334
342
  except ValueError:
335
343
  # Re-raise configuration validation errors (these are user-facing)
336
344
  raise
337
- except Exception: # nosec B112
338
- # Skip rules that fail (defensive programming)
345
+ except Exception:
346
+ logger.exception("Rule %s failed on %s", rule.rule_id, context.file_path)
339
347
  return []
340
348
 
341
349
  def lint_directory(self, dir_path: Path, recursive: bool = True) -> list[Violation]:
@@ -410,7 +418,8 @@ class Orchestrator: # thailint: ignore[srp]
410
418
  """Extract violations from a completed future, handling errors."""
411
419
  try:
412
420
  return [Violation.from_dict(d) for d in future.result()]
413
- except Exception: # nosec B112 - continue on worker errors
421
+ except Exception:
422
+ logger.exception("Error extracting violations from worker future")
414
423
  return []
415
424
 
416
425
  def _finalize_rules(self) -> list[Violation]: