thailint 0.5.0__py3-none-any.whl → 0.15.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
src/orchestrator/core.py CHANGED
@@ -10,34 +10,170 @@ Overview: Provides the main entry point for linting operations by coordinating e
10
10
  appropriate file information and language metadata, executes applicable rules against contexts,
11
11
  and collects violations across all processed files. Supports recursive and non-recursive
12
12
  directory traversal, respects .thailintignore patterns at repository level, and provides
13
- configurable linting through .thailint.yaml configuration files. Serves as the primary
14
- interface between the linter framework and user-facing CLI/library APIs.
13
+ configurable linting through .thailint.yaml configuration files. Includes parallel processing
14
+ support for improved performance on multi-core systems. Serves as the primary interface between
15
+ the linter framework and user-facing CLI/library APIs.
15
16
 
16
17
  Dependencies: pathlib for file operations, BaseLintRule and BaseLintContext from core.base,
17
18
  Violation from core.types, RuleRegistry from core.registry, LinterConfigLoader from
18
19
  linter_config.loader, IgnoreDirectiveParser from linter_config.ignore, detect_language
19
- from language_detector
20
+ from language_detector, concurrent.futures for parallel processing
20
21
 
21
22
  Exports: Orchestrator class, FileLintContext implementation class
22
23
 
23
24
  Interfaces: Orchestrator(project_root: Path | None), lint_file(file_path: Path) -> list[Violation],
24
- lint_directory(dir_path: Path, recursive: bool) -> list[Violation]
25
+ lint_directory(dir_path: Path, recursive: bool) -> list[Violation],
26
+ lint_files_parallel(file_paths, max_workers) -> list[Violation]
25
27
 
26
28
  Implementation: Directory glob pattern matching for traversal (** for recursive, * for shallow),
27
29
  ignore pattern checking before file processing, dynamic context creation per file,
28
- rule filtering by applicability, violation collection and aggregation across files
30
+ rule filtering by applicability, violation collection and aggregation across files,
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.
29
36
  """
30
37
 
38
+ from __future__ import annotations
39
+
40
+ import logging
41
+ import multiprocessing
42
+ import os
43
+ from concurrent.futures import Future, ProcessPoolExecutor, as_completed
31
44
  from pathlib import Path
32
45
 
33
46
  from src.core.base import BaseLintContext, BaseLintRule
34
47
  from src.core.registry import RuleRegistry
35
48
  from src.core.types import Violation
36
- from src.linter_config.ignore import IgnoreDirectiveParser
49
+ from src.linter_config.ignore import get_ignore_parser
37
50
  from src.linter_config.loader import LinterConfigLoader
38
51
 
39
52
  from .language_detector import detect_language
40
53
 
54
+ logger = logging.getLogger(__name__)
55
+
56
+ # Default max workers for parallel processing (capped to avoid resource contention)
57
+ DEFAULT_MAX_WORKERS = 8
58
+
59
+ # Hardcoded exclusions for files/directories that should never be linted
60
+ # These are always skipped regardless of configuration to improve performance
61
+ _HARDCODED_EXCLUDE_EXTENSIONS: frozenset[str] = frozenset(
62
+ {
63
+ ".pyc",
64
+ ".pyo",
65
+ ".pyd", # Python bytecode
66
+ ".so",
67
+ ".dll",
68
+ ".dylib", # Compiled libraries
69
+ ".class", # Java bytecode
70
+ ".o",
71
+ ".obj", # Object files
72
+ }
73
+ )
74
+ _HARDCODED_EXCLUDE_DIRS: frozenset[str] = frozenset(
75
+ {
76
+ "__pycache__",
77
+ "node_modules",
78
+ ".git",
79
+ ".svn",
80
+ ".hg",
81
+ ".venv",
82
+ "venv",
83
+ ".tox",
84
+ ".eggs",
85
+ "*.egg-info",
86
+ ".pytest_cache",
87
+ ".mypy_cache",
88
+ ".ruff_cache",
89
+ "dist",
90
+ "build",
91
+ "htmlcov",
92
+ }
93
+ )
94
+
95
+
96
+ def _is_hardcoded_excluded(file_path: Path) -> bool:
97
+ """Check if file should be excluded based on hardcoded patterns.
98
+
99
+ Args:
100
+ file_path: Path to check
101
+
102
+ Returns:
103
+ True if file should be skipped (compiled file, cache directory, etc.)
104
+ """
105
+ # Check file extension
106
+ if file_path.suffix in _HARDCODED_EXCLUDE_EXTENSIONS:
107
+ return True
108
+
109
+ # Check if any parent directory is in the exclude list
110
+ for part in file_path.parts:
111
+ if part in _HARDCODED_EXCLUDE_DIRS:
112
+ return True
113
+ # Handle wildcard patterns like *.egg-info
114
+ if part.endswith(".egg-info"):
115
+ return True
116
+
117
+ return False
118
+
119
+
120
+ def _should_include_dir(dirname: str) -> bool:
121
+ """Check if directory should be traversed (not excluded)."""
122
+ return dirname not in _HARDCODED_EXCLUDE_DIRS and not dirname.endswith(".egg-info")
123
+
124
+
125
+ def _collect_files_from_walk(root: str, filenames: list[str]) -> list[Path]:
126
+ """Collect non-excluded files from a single directory."""
127
+ root_path = Path(root)
128
+ return [root_path / f for f in filenames if Path(f).suffix not in _HARDCODED_EXCLUDE_EXTENSIONS]
129
+
130
+
131
+ def _collect_files_fast(dir_path: Path, recursive: bool = True) -> list[Path]:
132
+ """Collect files, skipping excluded directories entirely.
133
+
134
+ Uses os.walk() instead of glob to avoid traversing into excluded
135
+ directories like .venv, node_modules, __pycache__, etc.
136
+
137
+ Args:
138
+ dir_path: Directory to collect files from.
139
+ recursive: Whether to traverse subdirectories.
140
+
141
+ Returns:
142
+ List of file paths, excluding hardcoded exclusions.
143
+ """
144
+ files: list[Path] = []
145
+ for root, dirs, filenames in os.walk(dir_path):
146
+ dirs[:] = [d for d in dirs if _should_include_dir(d)]
147
+ files.extend(_collect_files_from_walk(root, filenames))
148
+ if not recursive:
149
+ break
150
+ return files
151
+
152
+
153
+ def _lint_file_worker(args: tuple[Path, Path, dict]) -> list[dict]:
154
+ """Worker function for parallel file linting.
155
+
156
+ This function runs in a separate process and creates its own Orchestrator
157
+ instance to lint a single file. Results are returned as dicts to avoid
158
+ pickling issues with Violation dataclass.
159
+
160
+ Args:
161
+ args: Tuple of (file_path, project_root, config)
162
+
163
+ Returns:
164
+ List of violation dicts (serializable for cross-process transfer)
165
+ """
166
+ file_path, project_root, config = args
167
+ try:
168
+ # Create isolated orchestrator for this worker process
169
+ orchestrator = Orchestrator(project_root=project_root, config=config)
170
+ violations = orchestrator.lint_file(file_path)
171
+ # Convert to dicts for pickling
172
+ return [v.to_dict() for v in violations]
173
+ except Exception:
174
+ logger.exception("Worker error processing file: %s", file_path)
175
+ return []
176
+
41
177
 
42
178
  class FileLintContext(BaseLintContext):
43
179
  """Concrete implementation of lint context for file analysis."""
@@ -56,6 +192,7 @@ class FileLintContext(BaseLintContext):
56
192
  self._path = path
57
193
  self._language = lang
58
194
  self._content = content
195
+ self._lines: list[str] | None = None # Cached line split
59
196
  self.metadata = metadata or {}
60
197
 
61
198
  @property
@@ -81,12 +218,31 @@ class FileLintContext(BaseLintContext):
81
218
  """Get programming language of file."""
82
219
  return self._language
83
220
 
221
+ @property
222
+ def file_lines(self) -> list[str]:
223
+ """Get file content as list of lines (cached).
224
+
225
+ Returns:
226
+ List of lines from file content, empty list if no content.
227
+ """
228
+ if self._lines is None:
229
+ content = self.file_content
230
+ self._lines = content.split("\n") if content else []
231
+ return self._lines
84
232
 
85
- class Orchestrator:
233
+
234
+ class Orchestrator: # thailint: ignore[srp]
86
235
  """Main linter orchestrator coordinating rule execution.
87
236
 
88
237
  Integrates rule registry, configuration loading, ignore patterns, and language
89
238
  detection to provide comprehensive linting of files and directories.
239
+
240
+ SRP Exception: Method count (12) exceeds guideline (8) because this is the
241
+ central orchestration point. Methods are organized into logical groups:
242
+ - Core linting: lint_file, lint_files, lint_directory
243
+ - Parallel linting: lint_files_parallel, lint_directory_parallel
244
+ - Helper methods: _execute_rules, _safe_check_rule, _ensure_rules_discovered, etc.
245
+ All methods support the single responsibility of coordinating lint operations.
90
246
  """
91
247
 
92
248
  def __init__(self, project_root: Path | None = None, config: dict | None = None):
@@ -99,7 +255,7 @@ class Orchestrator:
99
255
  self.project_root = project_root or Path.cwd()
100
256
  self.registry = RuleRegistry()
101
257
  self.config_loader = LinterConfigLoader()
102
- self.ignore_parser = IgnoreDirectiveParser(self.project_root)
258
+ self.ignore_parser = get_ignore_parser(self.project_root)
103
259
 
104
260
  # Performance optimization: Defer rule discovery until first file is linted
105
261
  # This eliminates ~0.077s overhead for commands that don't need rules (--help, config, etc.)
@@ -125,6 +281,10 @@ class Orchestrator:
125
281
  Returns:
126
282
  List of violations found in the file.
127
283
  """
284
+ # Fast path: skip compiled files and common excluded directories
285
+ if _is_hardcoded_excluded(file_path):
286
+ return []
287
+
128
288
  if self.ignore_parser.is_ignored(file_path):
129
289
  return []
130
290
 
@@ -182,8 +342,8 @@ class Orchestrator:
182
342
  except ValueError:
183
343
  # Re-raise configuration validation errors (these are user-facing)
184
344
  raise
185
- except Exception: # nosec B112
186
- # Skip rules that fail (defensive programming)
345
+ except Exception:
346
+ logger.exception("Rule %s failed on %s", rule.rule_id, context.file_path)
187
347
  return []
188
348
 
189
349
  def lint_directory(self, dir_path: Path, recursive: bool = True) -> list[Violation]:
@@ -197,11 +357,11 @@ class Orchestrator:
197
357
  List of all violations found across all files.
198
358
  """
199
359
  violations = []
200
- pattern = "**/*" if recursive else "*"
360
+ # Use fast file collection that skips excluded directories entirely
361
+ file_paths = _collect_files_fast(dir_path, recursive)
201
362
 
202
- for file_path in dir_path.glob(pattern):
203
- if file_path.is_file():
204
- violations.extend(self.lint_file(file_path))
363
+ for file_path in file_paths:
364
+ violations.extend(self.lint_file(file_path))
205
365
 
206
366
  # Call finalize() on all rules after processing all files
207
367
  for rule in self.registry.list_all():
@@ -209,6 +369,84 @@ class Orchestrator:
209
369
 
210
370
  return violations
211
371
 
372
+ def lint_files_parallel(
373
+ self, file_paths: list[Path], max_workers: int | None = None
374
+ ) -> list[Violation]:
375
+ """Lint multiple files in parallel using process pool.
376
+
377
+ Uses ProcessPoolExecutor to distribute file linting across multiple
378
+ CPU cores. Each worker process creates its own Orchestrator instance.
379
+
380
+ Args:
381
+ file_paths: List of file paths to lint.
382
+ max_workers: Maximum worker processes. Defaults to min(DEFAULT_MAX_WORKERS, cpu_count).
383
+
384
+ Returns:
385
+ List of violations found across all files.
386
+ """
387
+ if not file_paths:
388
+ return []
389
+
390
+ effective_workers = max_workers or min(DEFAULT_MAX_WORKERS, multiprocessing.cpu_count())
391
+
392
+ # For small file counts, sequential is faster due to process overhead
393
+ if len(file_paths) < effective_workers * 2:
394
+ return self.lint_files(file_paths)
395
+
396
+ violations = self._execute_parallel_linting(file_paths, effective_workers)
397
+ violations.extend(self._finalize_rules())
398
+ return violations
399
+
400
+ def _execute_parallel_linting(
401
+ self, file_paths: list[Path], max_workers: int
402
+ ) -> list[Violation]:
403
+ """Execute parallel linting using process pool."""
404
+ work_items = [(fp, self.project_root, self.config) for fp in file_paths]
405
+
406
+ with ProcessPoolExecutor(max_workers=max_workers) as executor:
407
+ futures = [executor.submit(_lint_file_worker, item) for item in work_items]
408
+ return self._collect_parallel_results(futures)
409
+
410
+ def _collect_parallel_results(self, futures: list[Future[list[dict]]]) -> list[Violation]:
411
+ """Collect results from parallel futures."""
412
+ violations: list[Violation] = []
413
+ for future in as_completed(futures):
414
+ violations.extend(self._extract_violations_from_future(future))
415
+ return violations
416
+
417
+ def _extract_violations_from_future(self, future: Future[list[dict]]) -> list[Violation]:
418
+ """Extract violations from a completed future, handling errors."""
419
+ try:
420
+ return [Violation.from_dict(d) for d in future.result()]
421
+ except Exception:
422
+ logger.exception("Error extracting violations from worker future")
423
+ return []
424
+
425
+ def _finalize_rules(self) -> list[Violation]:
426
+ """Call finalize() on all rules for cross-file analysis."""
427
+ self._ensure_rules_discovered()
428
+ violations: list[Violation] = []
429
+ for rule in self.registry.list_all():
430
+ violations.extend(rule.finalize())
431
+ return violations
432
+
433
+ def lint_directory_parallel(
434
+ self, dir_path: Path, recursive: bool = True, max_workers: int | None = None
435
+ ) -> list[Violation]:
436
+ """Lint all files in a directory using parallel processing.
437
+
438
+ Args:
439
+ dir_path: Path to directory to lint.
440
+ recursive: Whether to traverse subdirectories recursively.
441
+ max_workers: Maximum worker processes. Defaults to min(DEFAULT_MAX_WORKERS, cpu_count).
442
+
443
+ Returns:
444
+ List of all violations found across all files.
445
+ """
446
+ # Use fast file collection that skips excluded directories entirely
447
+ file_paths = _collect_files_fast(dir_path, recursive)
448
+ return self.lint_files_parallel(file_paths, max_workers=max_workers)
449
+
212
450
  def _ensure_rules_discovered(self) -> None:
213
451
  """Ensure rules have been discovered and registered (lazy initialization)."""
214
452
  if not self._rules_discovered:
@@ -17,12 +17,13 @@ Dependencies: pathlib for file path handling and content reading
17
17
  Exports: detect_language(file_path: Path) -> str function, EXTENSION_MAP constant
18
18
 
19
19
  Interfaces: detect_language(file_path: Path) -> str returns language identifier string
20
- (python, javascript, typescript, java, go, unknown)
20
+ (python, javascript, typescript, java, go, rust, unknown)
21
21
 
22
22
  Implementation: Dictionary-based extension lookup for O(1) detection, first-line shebang
23
23
  parsing with substring matching, lazy file reading only when extension unknown
24
24
  """
25
25
 
26
+ from contextlib import suppress
26
27
  from pathlib import Path
27
28
 
28
29
  # Extension to language mapping
@@ -34,6 +35,7 @@ EXTENSION_MAP = {
34
35
  ".jsx": "javascript",
35
36
  ".java": "java",
36
37
  ".go": "go",
38
+ ".rs": "rust",
37
39
  }
38
40
 
39
41
 
@@ -67,10 +69,10 @@ def detect_language(file_path: Path) -> str:
67
69
  file_path: Path to file to analyze.
68
70
 
69
71
  Returns:
70
- Language identifier (python, javascript, typescript, java, go, unknown).
72
+ Language identifier (python, javascript, typescript, java, go, rust, unknown).
71
73
  """
72
74
  ext = file_path.suffix.lower()
73
- if ext in EXTENSION_MAP:
75
+ with suppress(KeyError):
74
76
  return EXTENSION_MAP[ext]
75
77
 
76
78
  if file_path.exists() and file_path.stat().st_size > 0:
@@ -132,6 +132,202 @@ 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
+
301
+ # ============================================================================
302
+ # PERFORMANCE LINTER
303
+ # ============================================================================
304
+ # Detects performance anti-patterns in loops that cause O(n²) behavior
305
+ #
306
+ performance:
307
+ enabled: true
308
+
309
+ # String concatenation in loops (O(n²) pattern)
310
+ # Detects: result += str in for/while loops
311
+ # Suggests: Use "".join() or list append + join
312
+ string-concat-loop:
313
+ enabled: true
314
+ # Report each += separately, or one violation per loop
315
+ # Default: false (one per loop)
316
+ report_each_concat: false
317
+
318
+ # Regex compilation in loops
319
+ # Detects: re.match(), re.search(), re.sub() etc. in loops
320
+ # Suggests: Use re.compile() outside loop
321
+ regex-in-loop:
322
+ enabled: true
323
+
324
+ # -------------------------------------------------------------------------
325
+ # OPTIONAL: File patterns to ignore
326
+ # -------------------------------------------------------------------------
327
+ # ignore:
328
+ # - "tests/**"
329
+ # - "scripts/**"
330
+
135
331
  # ============================================================================
136
332
  # GLOBAL SETTINGS
137
333
  # ============================================================================
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