thailint 0.11.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 (129) 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 +118 -7
  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/utils.py +29 -9
  14. src/cli_main.py +3 -0
  15. src/config.py +2 -1
  16. src/core/base.py +3 -2
  17. src/core/cli_utils.py +3 -1
  18. src/core/config_parser.py +5 -2
  19. src/core/constants.py +54 -0
  20. src/core/linter_utils.py +4 -0
  21. src/core/rule_discovery.py +5 -1
  22. src/core/violation_builder.py +3 -0
  23. src/linter_config/directive_markers.py +109 -0
  24. src/linter_config/ignore.py +225 -383
  25. src/linter_config/pattern_utils.py +65 -0
  26. src/linter_config/rule_matcher.py +89 -0
  27. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  28. src/linters/collection_pipeline/ast_utils.py +40 -0
  29. src/linters/collection_pipeline/config.py +12 -0
  30. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  31. src/linters/collection_pipeline/detector.py +262 -32
  32. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  33. src/linters/collection_pipeline/linter.py +18 -35
  34. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  35. src/linters/dry/base_token_analyzer.py +16 -9
  36. src/linters/dry/block_filter.py +7 -4
  37. src/linters/dry/cache.py +7 -2
  38. src/linters/dry/config.py +7 -1
  39. src/linters/dry/constant_matcher.py +34 -25
  40. src/linters/dry/file_analyzer.py +4 -2
  41. src/linters/dry/inline_ignore.py +7 -16
  42. src/linters/dry/linter.py +48 -25
  43. src/linters/dry/python_analyzer.py +18 -10
  44. src/linters/dry/python_constant_extractor.py +51 -52
  45. src/linters/dry/single_statement_detector.py +14 -12
  46. src/linters/dry/token_hasher.py +115 -115
  47. src/linters/dry/typescript_analyzer.py +11 -6
  48. src/linters/dry/typescript_constant_extractor.py +4 -0
  49. src/linters/dry/typescript_statement_detector.py +208 -208
  50. src/linters/dry/typescript_value_extractor.py +3 -0
  51. src/linters/dry/violation_filter.py +1 -4
  52. src/linters/dry/violation_generator.py +1 -4
  53. src/linters/file_header/atemporal_detector.py +4 -0
  54. src/linters/file_header/base_parser.py +4 -0
  55. src/linters/file_header/bash_parser.py +4 -0
  56. src/linters/file_header/field_validator.py +5 -8
  57. src/linters/file_header/linter.py +19 -12
  58. src/linters/file_header/markdown_parser.py +6 -0
  59. src/linters/file_placement/config_loader.py +3 -1
  60. src/linters/file_placement/linter.py +22 -8
  61. src/linters/file_placement/pattern_matcher.py +21 -4
  62. src/linters/file_placement/pattern_validator.py +21 -7
  63. src/linters/file_placement/rule_checker.py +2 -2
  64. src/linters/lazy_ignores/__init__.py +43 -0
  65. src/linters/lazy_ignores/config.py +66 -0
  66. src/linters/lazy_ignores/directive_utils.py +121 -0
  67. src/linters/lazy_ignores/header_parser.py +177 -0
  68. src/linters/lazy_ignores/linter.py +158 -0
  69. src/linters/lazy_ignores/matcher.py +135 -0
  70. src/linters/lazy_ignores/python_analyzer.py +201 -0
  71. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  72. src/linters/lazy_ignores/skip_detector.py +298 -0
  73. src/linters/lazy_ignores/types.py +67 -0
  74. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  75. src/linters/lazy_ignores/violation_builder.py +131 -0
  76. src/linters/lbyl/__init__.py +29 -0
  77. src/linters/lbyl/config.py +63 -0
  78. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  79. src/linters/lbyl/pattern_detectors/base.py +46 -0
  80. src/linters/magic_numbers/context_analyzer.py +227 -229
  81. src/linters/magic_numbers/linter.py +20 -15
  82. src/linters/magic_numbers/python_analyzer.py +4 -16
  83. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  84. src/linters/method_property/config.py +4 -0
  85. src/linters/method_property/linter.py +5 -4
  86. src/linters/method_property/python_analyzer.py +5 -4
  87. src/linters/method_property/violation_builder.py +3 -0
  88. src/linters/nesting/typescript_analyzer.py +6 -12
  89. src/linters/nesting/typescript_function_extractor.py +0 -4
  90. src/linters/print_statements/linter.py +6 -4
  91. src/linters/print_statements/python_analyzer.py +85 -81
  92. src/linters/print_statements/typescript_analyzer.py +6 -15
  93. src/linters/srp/heuristics.py +4 -4
  94. src/linters/srp/linter.py +12 -12
  95. src/linters/srp/violation_builder.py +0 -4
  96. src/linters/stateless_class/linter.py +30 -36
  97. src/linters/stateless_class/python_analyzer.py +11 -20
  98. src/linters/stringly_typed/__init__.py +22 -9
  99. src/linters/stringly_typed/config.py +32 -8
  100. src/linters/stringly_typed/context_filter.py +451 -0
  101. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  102. src/linters/stringly_typed/ignore_checker.py +102 -0
  103. src/linters/stringly_typed/ignore_utils.py +51 -0
  104. src/linters/stringly_typed/linter.py +376 -0
  105. src/linters/stringly_typed/python/__init__.py +9 -5
  106. src/linters/stringly_typed/python/analyzer.py +159 -9
  107. src/linters/stringly_typed/python/call_tracker.py +175 -0
  108. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  109. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  110. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  111. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  112. src/linters/stringly_typed/python/validation_detector.py +3 -0
  113. src/linters/stringly_typed/storage.py +630 -0
  114. src/linters/stringly_typed/storage_initializer.py +45 -0
  115. src/linters/stringly_typed/typescript/__init__.py +28 -0
  116. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  117. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  118. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  119. src/linters/stringly_typed/violation_generator.py +405 -0
  120. src/orchestrator/core.py +13 -4
  121. src/templates/thailint_config_template.yaml +166 -0
  122. src/utils/project_root.py +3 -0
  123. thailint-0.13.0.dist-info/METADATA +184 -0
  124. thailint-0.13.0.dist-info/RECORD +189 -0
  125. thailint-0.11.0.dist-info/METADATA +0 -1661
  126. thailint-0.11.0.dist-info/RECORD +0 -150
  127. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
  128. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
  129. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,405 @@
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 _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:
325
+ """Generates violations from cross-file stringly-typed patterns."""
326
+
327
+ def __init__(self) -> None:
328
+ """Initialize with helper filters."""
329
+ self._ignore_checker = IgnoreChecker()
330
+
331
+ def generate_violations(
332
+ self,
333
+ storage: StringlyTypedStorage,
334
+ rule_id: str,
335
+ config: StringlyTypedConfig,
336
+ ) -> list[Violation]:
337
+ """Generate violations from storage.
338
+
339
+ Args:
340
+ storage: Pattern storage instance
341
+ rule_id: Rule identifier for violations
342
+ config: Stringly-typed configuration with thresholds
343
+
344
+ Returns:
345
+ List of violations for patterns appearing in multiple files
346
+ """
347
+ violations: list[Violation] = []
348
+ pattern_violations, covered_vars = self._generate_pattern_violations(
349
+ storage, rule_id, config
350
+ )
351
+ violations.extend(pattern_violations)
352
+ violations.extend(self._generate_function_call_violations(storage, config))
353
+ violations.extend(self._generate_comparison_violations(storage, config, covered_vars))
354
+
355
+ # Apply path-based ignore patterns from config
356
+ violations = _filter_by_ignore(violations, config.ignore)
357
+
358
+ # Apply inline ignore directives via IgnoreChecker
359
+ violations = self._ignore_checker.filter_violations(violations)
360
+
361
+ return violations
362
+
363
+ def _generate_pattern_violations(
364
+ self,
365
+ storage: StringlyTypedStorage,
366
+ rule_id: str,
367
+ config: StringlyTypedConfig,
368
+ ) -> tuple[list[Violation], set[str]]:
369
+ """Generate violations for duplicate validation patterns."""
370
+ duplicate_hashes = storage.get_duplicate_hashes(min_files=config.min_occurrences)
371
+ violations: list[Violation] = []
372
+ covered_variables: set[str] = set()
373
+
374
+ for hash_value in duplicate_hashes:
375
+ patterns = storage.get_patterns_by_hash(hash_value)
376
+ _process_pattern_group(patterns, config, rule_id, violations, covered_variables)
377
+
378
+ return violations, covered_variables
379
+
380
+ def _generate_function_call_violations(
381
+ self,
382
+ storage: StringlyTypedStorage,
383
+ config: StringlyTypedConfig,
384
+ ) -> list[Violation]:
385
+ """Generate violations for function call patterns."""
386
+ valid_funcs = _get_valid_functions(storage, config)
387
+ return _build_call_violations(valid_funcs, storage)
388
+
389
+ def _generate_comparison_violations(
390
+ self,
391
+ storage: StringlyTypedStorage,
392
+ config: StringlyTypedConfig,
393
+ covered_variables: set[str] | None = None,
394
+ ) -> list[Violation]:
395
+ """Generate violations for scattered string comparisons."""
396
+ covered_variables = covered_variables or set()
397
+ variables = _get_variables_to_check(storage, config)
398
+
399
+ violations: list[Violation] = []
400
+ for variable_name, unique_values in variables:
401
+ _process_variable(
402
+ variable_name, unique_values, storage, config, covered_variables, violations
403
+ )
404
+
405
+ return violations
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]:
@@ -132,6 +132,172 @@ print-statements:
132
132
  # - "scripts/**"
133
133
  # - "**/debug.py"
134
134
 
135
+ # ============================================================================
136
+ # STRINGLY-TYPED LINTER
137
+ # ============================================================================
138
+ # Detects "stringly typed" code patterns where strings are used instead of
139
+ # proper enums - e.g., if env == "production": ... repeated across files
140
+ #
141
+ stringly-typed:
142
+ enabled: true
143
+
144
+ # Minimum occurrences across files to flag a violation
145
+ # Default: 2
146
+ min_occurrences: 2
147
+
148
+ # Minimum unique string values to suggest creating an enum
149
+ # Default: 2
150
+ min_values_for_enum: 2
151
+
152
+ # Maximum unique string values to suggest an enum (above this, probably not enum-worthy)
153
+ # Default: 6
154
+ max_values_for_enum: 6
155
+
156
+ # Whether to require cross-file occurrences to flag violations
157
+ # Default: true
158
+ require_cross_file: true
159
+
160
+ # -------------------------------------------------------------------------
161
+ # OPTIONAL: String value sets that are acceptable (won't be flagged)
162
+ # -------------------------------------------------------------------------
163
+ # allowed_string_sets:
164
+ # - ["debug", "info", "warning", "error"] # Log levels
165
+ # - ["ASC", "DESC"] # Sort directions
166
+
167
+ # -------------------------------------------------------------------------
168
+ # OPTIONAL: Variable names to exclude from analysis
169
+ # -------------------------------------------------------------------------
170
+ # exclude_variables:
171
+ # - log_level
172
+ # - severity
173
+
174
+ # ============================================================================
175
+ # FILE HEADER LINTER
176
+ # ============================================================================
177
+ # Validates that files have proper documentation headers
178
+ #
179
+ file-header:
180
+ enabled: true
181
+
182
+ # Enforce atemporal language (no "currently", "now", dates)
183
+ # Default: true
184
+ enforce_atemporal: true
185
+
186
+ # -------------------------------------------------------------------------
187
+ # OPTIONAL: Override required fields by language
188
+ # -------------------------------------------------------------------------
189
+ # required_fields:
190
+ # python: [Purpose, Scope, Overview, Dependencies, Exports, Interfaces, Implementation]
191
+ # typescript: [Purpose, Scope, Overview, Dependencies, Exports, Props/Interfaces, State/Behavior]
192
+ # bash: [Purpose, Scope, Overview, Dependencies, Exports, Usage, Environment]
193
+ # markdown: [purpose, scope, overview, audience, status]
194
+ # css: [Purpose, Scope, Overview, Dependencies, Exports, Interfaces, Environment]
195
+
196
+ # -------------------------------------------------------------------------
197
+ # OPTIONAL: File patterns to ignore
198
+ # -------------------------------------------------------------------------
199
+ # ignore:
200
+ # - "test/**"
201
+ # - "**/migrations/**"
202
+ # - "**/__init__.py"
203
+
204
+ # ============================================================================
205
+ # METHOD PROPERTY LINTER
206
+ # ============================================================================
207
+ # Detects methods that should be @property (no args, simple return)
208
+ #
209
+ method-property:
210
+ enabled: true
211
+
212
+ # Maximum statements in method body to suggest @property
213
+ # Default: 3
214
+ max_body_statements: 3
215
+
216
+ # -------------------------------------------------------------------------
217
+ # OPTIONAL: Methods to ignore (exact names)
218
+ # -------------------------------------------------------------------------
219
+ # ignore_methods:
220
+ # - "__str__"
221
+ # - "__repr__"
222
+
223
+ # -------------------------------------------------------------------------
224
+ # OPTIONAL: Additional action verb prefixes to exclude
225
+ # -------------------------------------------------------------------------
226
+ # exclude_prefixes:
227
+ # - "fetch_"
228
+ # - "load_"
229
+
230
+ # -------------------------------------------------------------------------
231
+ # OPTIONAL: File patterns to ignore
232
+ # -------------------------------------------------------------------------
233
+ # ignore:
234
+ # - "tests/**"
235
+
236
+ # ============================================================================
237
+ # STATELESS CLASS LINTER
238
+ # ============================================================================
239
+ # Detects classes with no instance state (should be modules or functions)
240
+ #
241
+ stateless-class:
242
+ enabled: true
243
+
244
+ # Minimum methods to flag a stateless class
245
+ # Default: 2
246
+ min_methods: 2
247
+
248
+ # -------------------------------------------------------------------------
249
+ # OPTIONAL: File patterns to ignore
250
+ # -------------------------------------------------------------------------
251
+ # ignore:
252
+ # - "tests/**"
253
+
254
+ # ============================================================================
255
+ # COLLECTION PIPELINE LINTER
256
+ # ============================================================================
257
+ # Detects "embedded loop filtering" anti-pattern (if/continue in loops)
258
+ # Suggests using filter(), list comprehensions, or generator expressions
259
+ #
260
+ pipeline:
261
+ enabled: true
262
+
263
+ # Minimum if/continue patterns in a loop to flag
264
+ # Default: 1
265
+ min_continues: 1
266
+
267
+ # -------------------------------------------------------------------------
268
+ # OPTIONAL: File patterns to ignore
269
+ # -------------------------------------------------------------------------
270
+ # ignore:
271
+ # - "tests/**"
272
+
273
+ # ============================================================================
274
+ # LAZY IGNORES LINTER
275
+ # ============================================================================
276
+ # Detects unjustified linting suppressions (noqa, type: ignore, etc.)
277
+ # without proper documentation in file headers
278
+ #
279
+ lazy-ignores:
280
+ enabled: true
281
+
282
+ # Pattern-specific toggles
283
+ check_noqa: true
284
+ check_type_ignore: true
285
+ check_pylint_disable: true
286
+ check_nosec: true
287
+ check_ts_ignore: true
288
+ check_eslint_disable: true
289
+ check_thailint_ignore: true
290
+ check_test_skips: true
291
+
292
+ # Check for orphaned suppressions (documented but not used)
293
+ check_orphaned: true
294
+
295
+ # -------------------------------------------------------------------------
296
+ # OPTIONAL: File patterns to ignore
297
+ # -------------------------------------------------------------------------
298
+ # ignore_patterns:
299
+ # - "tests/**"
300
+
135
301
  # ============================================================================
136
302
  # GLOBAL SETTINGS
137
303
  # ============================================================================
src/utils/project_root.py CHANGED
@@ -15,6 +15,9 @@ Exports: is_project_root(), get_project_root()
15
15
  Interfaces: Path-based functions for checking and finding project roots
16
16
 
17
17
  Implementation: pyprojroot delegation with manual fallback for test environments
18
+
19
+ Suppressions:
20
+ - type:ignore[arg-type]: pyprojroot external library typing issue with Path conversion
18
21
  """
19
22
 
20
23
  from pathlib import Path