thailint 0.9.0__py3-none-any.whl → 0.11.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 (69) hide show
  1. src/__init__.py +1 -0
  2. src/cli/__init__.py +27 -0
  3. src/cli/__main__.py +22 -0
  4. src/cli/config.py +478 -0
  5. src/cli/linters/__init__.py +58 -0
  6. src/cli/linters/code_patterns.py +372 -0
  7. src/cli/linters/code_smells.py +343 -0
  8. src/cli/linters/documentation.py +155 -0
  9. src/cli/linters/shared.py +89 -0
  10. src/cli/linters/structure.py +313 -0
  11. src/cli/linters/structure_quality.py +316 -0
  12. src/cli/main.py +120 -0
  13. src/cli/utils.py +375 -0
  14. src/cli_main.py +34 -0
  15. src/config.py +2 -3
  16. src/core/rule_discovery.py +43 -10
  17. src/core/types.py +13 -0
  18. src/core/violation_utils.py +69 -0
  19. src/linter_config/ignore.py +32 -16
  20. src/linters/collection_pipeline/__init__.py +90 -0
  21. src/linters/collection_pipeline/config.py +63 -0
  22. src/linters/collection_pipeline/continue_analyzer.py +100 -0
  23. src/linters/collection_pipeline/detector.py +130 -0
  24. src/linters/collection_pipeline/linter.py +437 -0
  25. src/linters/collection_pipeline/suggestion_builder.py +63 -0
  26. src/linters/dry/block_filter.py +99 -9
  27. src/linters/dry/cache.py +94 -6
  28. src/linters/dry/config.py +47 -10
  29. src/linters/dry/constant.py +92 -0
  30. src/linters/dry/constant_matcher.py +214 -0
  31. src/linters/dry/constant_violation_builder.py +98 -0
  32. src/linters/dry/linter.py +89 -48
  33. src/linters/dry/python_analyzer.py +44 -431
  34. src/linters/dry/python_constant_extractor.py +101 -0
  35. src/linters/dry/single_statement_detector.py +415 -0
  36. src/linters/dry/token_hasher.py +5 -5
  37. src/linters/dry/typescript_analyzer.py +63 -382
  38. src/linters/dry/typescript_constant_extractor.py +134 -0
  39. src/linters/dry/typescript_statement_detector.py +255 -0
  40. src/linters/dry/typescript_value_extractor.py +66 -0
  41. src/linters/file_header/linter.py +9 -13
  42. src/linters/file_placement/linter.py +30 -10
  43. src/linters/file_placement/pattern_matcher.py +19 -5
  44. src/linters/magic_numbers/linter.py +8 -67
  45. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  46. src/linters/nesting/linter.py +12 -9
  47. src/linters/print_statements/linter.py +7 -24
  48. src/linters/srp/class_analyzer.py +9 -9
  49. src/linters/srp/heuristics.py +6 -5
  50. src/linters/srp/linter.py +4 -5
  51. src/linters/stateless_class/linter.py +2 -2
  52. src/linters/stringly_typed/__init__.py +23 -0
  53. src/linters/stringly_typed/config.py +165 -0
  54. src/linters/stringly_typed/python/__init__.py +29 -0
  55. src/linters/stringly_typed/python/analyzer.py +198 -0
  56. src/linters/stringly_typed/python/condition_extractor.py +131 -0
  57. src/linters/stringly_typed/python/conditional_detector.py +176 -0
  58. src/linters/stringly_typed/python/constants.py +21 -0
  59. src/linters/stringly_typed/python/match_analyzer.py +88 -0
  60. src/linters/stringly_typed/python/validation_detector.py +186 -0
  61. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  62. src/orchestrator/core.py +241 -12
  63. {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/METADATA +116 -3
  64. {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/RECORD +67 -29
  65. thailint-0.11.0.dist-info/entry_points.txt +4 -0
  66. src/cli.py +0 -2014
  67. thailint-0.9.0.dist-info/entry_points.txt +0 -4
  68. {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/WHEEL +0 -0
  69. {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/licenses/LICENSE +0 -0
src/orchestrator/core.py CHANGED
@@ -10,34 +10,162 @@ 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
29
32
  """
30
33
 
34
+ from __future__ import annotations
35
+
36
+ import multiprocessing
37
+ import os
38
+ from concurrent.futures import Future, ProcessPoolExecutor, as_completed
31
39
  from pathlib import Path
32
40
 
33
41
  from src.core.base import BaseLintContext, BaseLintRule
34
42
  from src.core.registry import RuleRegistry
35
43
  from src.core.types import Violation
36
- from src.linter_config.ignore import IgnoreDirectiveParser
44
+ from src.linter_config.ignore import get_ignore_parser
37
45
  from src.linter_config.loader import LinterConfigLoader
38
46
 
39
47
  from .language_detector import detect_language
40
48
 
49
+ # Default max workers for parallel processing (capped to avoid resource contention)
50
+ DEFAULT_MAX_WORKERS = 8
51
+
52
+ # Hardcoded exclusions for files/directories that should never be linted
53
+ # These are always skipped regardless of configuration to improve performance
54
+ _HARDCODED_EXCLUDE_EXTENSIONS: frozenset[str] = frozenset(
55
+ {
56
+ ".pyc",
57
+ ".pyo",
58
+ ".pyd", # Python bytecode
59
+ ".so",
60
+ ".dll",
61
+ ".dylib", # Compiled libraries
62
+ ".class", # Java bytecode
63
+ ".o",
64
+ ".obj", # Object files
65
+ }
66
+ )
67
+ _HARDCODED_EXCLUDE_DIRS: frozenset[str] = frozenset(
68
+ {
69
+ "__pycache__",
70
+ "node_modules",
71
+ ".git",
72
+ ".svn",
73
+ ".hg",
74
+ ".venv",
75
+ "venv",
76
+ ".tox",
77
+ ".eggs",
78
+ "*.egg-info",
79
+ ".pytest_cache",
80
+ ".mypy_cache",
81
+ ".ruff_cache",
82
+ "dist",
83
+ "build",
84
+ "htmlcov",
85
+ }
86
+ )
87
+
88
+
89
+ def _is_hardcoded_excluded(file_path: Path) -> bool:
90
+ """Check if file should be excluded based on hardcoded patterns.
91
+
92
+ Args:
93
+ file_path: Path to check
94
+
95
+ Returns:
96
+ True if file should be skipped (compiled file, cache directory, etc.)
97
+ """
98
+ # Check file extension
99
+ if file_path.suffix in _HARDCODED_EXCLUDE_EXTENSIONS:
100
+ return True
101
+
102
+ # Check if any parent directory is in the exclude list
103
+ for part in file_path.parts:
104
+ if part in _HARDCODED_EXCLUDE_DIRS:
105
+ return True
106
+ # Handle wildcard patterns like *.egg-info
107
+ if part.endswith(".egg-info"):
108
+ return True
109
+
110
+ return False
111
+
112
+
113
+ def _should_include_dir(dirname: str) -> bool:
114
+ """Check if directory should be traversed (not excluded)."""
115
+ return dirname not in _HARDCODED_EXCLUDE_DIRS and not dirname.endswith(".egg-info")
116
+
117
+
118
+ def _collect_files_from_walk(root: str, filenames: list[str]) -> list[Path]:
119
+ """Collect non-excluded files from a single directory."""
120
+ root_path = Path(root)
121
+ return [root_path / f for f in filenames if Path(f).suffix not in _HARDCODED_EXCLUDE_EXTENSIONS]
122
+
123
+
124
+ def _collect_files_fast(dir_path: Path, recursive: bool = True) -> list[Path]:
125
+ """Collect files, skipping excluded directories entirely.
126
+
127
+ Uses os.walk() instead of glob to avoid traversing into excluded
128
+ directories like .venv, node_modules, __pycache__, etc.
129
+
130
+ Args:
131
+ dir_path: Directory to collect files from.
132
+ recursive: Whether to traverse subdirectories.
133
+
134
+ Returns:
135
+ List of file paths, excluding hardcoded exclusions.
136
+ """
137
+ files: list[Path] = []
138
+ for root, dirs, filenames in os.walk(dir_path):
139
+ dirs[:] = [d for d in dirs if _should_include_dir(d)]
140
+ files.extend(_collect_files_from_walk(root, filenames))
141
+ if not recursive:
142
+ break
143
+ return files
144
+
145
+
146
+ def _lint_file_worker(args: tuple[Path, Path, dict]) -> list[dict]:
147
+ """Worker function for parallel file linting.
148
+
149
+ This function runs in a separate process and creates its own Orchestrator
150
+ instance to lint a single file. Results are returned as dicts to avoid
151
+ pickling issues with Violation dataclass.
152
+
153
+ Args:
154
+ args: Tuple of (file_path, project_root, config)
155
+
156
+ Returns:
157
+ List of violation dicts (serializable for cross-process transfer)
158
+ """
159
+ file_path, project_root, config = args
160
+ try:
161
+ # Create isolated orchestrator for this worker process
162
+ orchestrator = Orchestrator(project_root=project_root, config=config)
163
+ violations = orchestrator.lint_file(file_path)
164
+ # Convert to dicts for pickling
165
+ return [v.to_dict() for v in violations]
166
+ except Exception: # nosec B112 - defensive; don't crash on worker errors
167
+ return []
168
+
41
169
 
42
170
  class FileLintContext(BaseLintContext):
43
171
  """Concrete implementation of lint context for file analysis."""
@@ -56,6 +184,7 @@ class FileLintContext(BaseLintContext):
56
184
  self._path = path
57
185
  self._language = lang
58
186
  self._content = content
187
+ self._lines: list[str] | None = None # Cached line split
59
188
  self.metadata = metadata or {}
60
189
 
61
190
  @property
@@ -81,12 +210,31 @@ class FileLintContext(BaseLintContext):
81
210
  """Get programming language of file."""
82
211
  return self._language
83
212
 
213
+ @property
214
+ def file_lines(self) -> list[str]:
215
+ """Get file content as list of lines (cached).
216
+
217
+ Returns:
218
+ List of lines from file content, empty list if no content.
219
+ """
220
+ if self._lines is None:
221
+ content = self.file_content
222
+ self._lines = content.split("\n") if content else []
223
+ return self._lines
224
+
84
225
 
85
- class Orchestrator:
226
+ class Orchestrator: # thailint: ignore[srp]
86
227
  """Main linter orchestrator coordinating rule execution.
87
228
 
88
229
  Integrates rule registry, configuration loading, ignore patterns, and language
89
230
  detection to provide comprehensive linting of files and directories.
231
+
232
+ SRP Exception: Method count (12) exceeds guideline (8) because this is the
233
+ central orchestration point. Methods are organized into logical groups:
234
+ - Core linting: lint_file, lint_files, lint_directory
235
+ - Parallel linting: lint_files_parallel, lint_directory_parallel
236
+ - Helper methods: _execute_rules, _safe_check_rule, _ensure_rules_discovered, etc.
237
+ All methods support the single responsibility of coordinating lint operations.
90
238
  """
91
239
 
92
240
  def __init__(self, project_root: Path | None = None, config: dict | None = None):
@@ -99,7 +247,7 @@ class Orchestrator:
99
247
  self.project_root = project_root or Path.cwd()
100
248
  self.registry = RuleRegistry()
101
249
  self.config_loader = LinterConfigLoader()
102
- self.ignore_parser = IgnoreDirectiveParser(self.project_root)
250
+ self.ignore_parser = get_ignore_parser(self.project_root)
103
251
 
104
252
  # Performance optimization: Defer rule discovery until first file is linted
105
253
  # This eliminates ~0.077s overhead for commands that don't need rules (--help, config, etc.)
@@ -125,6 +273,10 @@ class Orchestrator:
125
273
  Returns:
126
274
  List of violations found in the file.
127
275
  """
276
+ # Fast path: skip compiled files and common excluded directories
277
+ if _is_hardcoded_excluded(file_path):
278
+ return []
279
+
128
280
  if self.ignore_parser.is_ignored(file_path):
129
281
  return []
130
282
 
@@ -197,11 +349,11 @@ class Orchestrator:
197
349
  List of all violations found across all files.
198
350
  """
199
351
  violations = []
200
- pattern = "**/*" if recursive else "*"
352
+ # Use fast file collection that skips excluded directories entirely
353
+ file_paths = _collect_files_fast(dir_path, recursive)
201
354
 
202
- for file_path in dir_path.glob(pattern):
203
- if file_path.is_file():
204
- violations.extend(self.lint_file(file_path))
355
+ for file_path in file_paths:
356
+ violations.extend(self.lint_file(file_path))
205
357
 
206
358
  # Call finalize() on all rules after processing all files
207
359
  for rule in self.registry.list_all():
@@ -209,6 +361,83 @@ class Orchestrator:
209
361
 
210
362
  return violations
211
363
 
364
+ def lint_files_parallel(
365
+ self, file_paths: list[Path], max_workers: int | None = None
366
+ ) -> list[Violation]:
367
+ """Lint multiple files in parallel using process pool.
368
+
369
+ Uses ProcessPoolExecutor to distribute file linting across multiple
370
+ CPU cores. Each worker process creates its own Orchestrator instance.
371
+
372
+ Args:
373
+ file_paths: List of file paths to lint.
374
+ max_workers: Maximum worker processes. Defaults to min(DEFAULT_MAX_WORKERS, cpu_count).
375
+
376
+ Returns:
377
+ List of violations found across all files.
378
+ """
379
+ if not file_paths:
380
+ return []
381
+
382
+ effective_workers = max_workers or min(DEFAULT_MAX_WORKERS, multiprocessing.cpu_count())
383
+
384
+ # For small file counts, sequential is faster due to process overhead
385
+ if len(file_paths) < effective_workers * 2:
386
+ return self.lint_files(file_paths)
387
+
388
+ violations = self._execute_parallel_linting(file_paths, effective_workers)
389
+ violations.extend(self._finalize_rules())
390
+ return violations
391
+
392
+ def _execute_parallel_linting(
393
+ self, file_paths: list[Path], max_workers: int
394
+ ) -> list[Violation]:
395
+ """Execute parallel linting using process pool."""
396
+ work_items = [(fp, self.project_root, self.config) for fp in file_paths]
397
+
398
+ with ProcessPoolExecutor(max_workers=max_workers) as executor:
399
+ futures = [executor.submit(_lint_file_worker, item) for item in work_items]
400
+ return self._collect_parallel_results(futures)
401
+
402
+ def _collect_parallel_results(self, futures: list[Future[list[dict]]]) -> list[Violation]:
403
+ """Collect results from parallel futures."""
404
+ violations: list[Violation] = []
405
+ for future in as_completed(futures):
406
+ violations.extend(self._extract_violations_from_future(future))
407
+ return violations
408
+
409
+ def _extract_violations_from_future(self, future: Future[list[dict]]) -> list[Violation]:
410
+ """Extract violations from a completed future, handling errors."""
411
+ try:
412
+ return [Violation.from_dict(d) for d in future.result()]
413
+ except Exception: # nosec B112 - continue on worker errors
414
+ return []
415
+
416
+ def _finalize_rules(self) -> list[Violation]:
417
+ """Call finalize() on all rules for cross-file analysis."""
418
+ self._ensure_rules_discovered()
419
+ violations: list[Violation] = []
420
+ for rule in self.registry.list_all():
421
+ violations.extend(rule.finalize())
422
+ return violations
423
+
424
+ def lint_directory_parallel(
425
+ self, dir_path: Path, recursive: bool = True, max_workers: int | None = None
426
+ ) -> list[Violation]:
427
+ """Lint all files in a directory using parallel processing.
428
+
429
+ Args:
430
+ dir_path: Path to directory to lint.
431
+ recursive: Whether to traverse subdirectories recursively.
432
+ max_workers: Maximum worker processes. Defaults to min(DEFAULT_MAX_WORKERS, cpu_count).
433
+
434
+ Returns:
435
+ List of all violations found across all files.
436
+ """
437
+ # Use fast file collection that skips excluded directories entirely
438
+ file_paths = _collect_files_fast(dir_path, recursive)
439
+ return self.lint_files_parallel(file_paths, max_workers=max_workers)
440
+
212
441
  def _ensure_rules_discovered(self) -> None:
213
442
  """Ensure rules have been discovered and registered (lazy initialization)."""
214
443
  if not self._rules_discovered:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thailint
3
- Version: 0.9.0
3
+ Version: 0.11.0
4
4
  Summary: The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -37,8 +37,8 @@ Description-Content-Type: text/markdown
37
37
 
38
38
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
39
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
40
- [![Tests](https://img.shields.io/badge/tests-728%2F728%20passing-brightgreen.svg)](tests/)
41
- [![Coverage](https://img.shields.io/badge/coverage-87%25-brightgreen.svg)](htmlcov/)
40
+ [![Tests](https://img.shields.io/badge/tests-884%2F884%20passing-brightgreen.svg)](tests/)
41
+ [![Coverage](https://img.shields.io/badge/coverage-88%25-brightgreen.svg)](htmlcov/)
42
42
  [![Documentation Status](https://readthedocs.org/projects/thai-lint/badge/?version=latest)](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
43
43
  [![SARIF 2.1.0](https://img.shields.io/badge/SARIF-2.1.0-orange.svg)](docs/sarif-output.md)
44
44
 
@@ -98,6 +98,12 @@ thailint complements your existing linting stack by catching the patterns AI too
98
98
  - Configurable thresholds (lines, tokens, occurrences)
99
99
  - Language-specific detection (Python, TypeScript, JavaScript)
100
100
  - False positive filtering (keyword args, imports)
101
+ - **Collection Pipeline Linting** - Detect for loops with embedded filtering
102
+ - Based on Martin Fowler's "Replace Loop with Pipeline" refactoring
103
+ - Detects if/continue patterns that should use generator expressions
104
+ - Generates refactoring suggestions with generator syntax
105
+ - Configurable threshold (min_continues)
106
+ - Python support with AST analysis
101
107
  - **Method Property Linting** - Detect methods that should be @property decorators
102
108
  - Python AST-based detection
103
109
  - get_* prefix detection (Java-style getters)
@@ -741,6 +747,109 @@ Built-in filters automatically exclude common non-duplication patterns:
741
747
 
742
748
  See [DRY Linter Guide](https://thai-lint.readthedocs.io/en/latest/dry-linter/) for comprehensive documentation, storage modes, and refactoring patterns.
743
749
 
750
+ ## Collection Pipeline Linter
751
+
752
+ ### Overview
753
+
754
+ The collection-pipeline linter detects for loops with embedded filtering (if/continue patterns) that should be refactored to use generator expressions or other collection pipelines. Based on Martin Fowler's "Replace Loop with Pipeline" refactoring pattern.
755
+
756
+ ### The Anti-Pattern
757
+
758
+ ```python
759
+ # Anti-pattern: Embedded filtering in loop body
760
+ for file_path in dir_path.glob(pattern):
761
+ if not file_path.is_file():
762
+ continue
763
+ if ignore_parser.is_ignored(file_path):
764
+ continue
765
+ violations.extend(lint_file(file_path))
766
+ ```
767
+
768
+ ### The Solution
769
+
770
+ ```python
771
+ # Collection pipeline: Filtering separated from processing
772
+ valid_files = (
773
+ f for f in dir_path.glob(pattern)
774
+ if f.is_file() and not ignore_parser.is_ignored(f)
775
+ )
776
+ for file_path in valid_files:
777
+ violations.extend(lint_file(file_path))
778
+ ```
779
+
780
+ ### Quick Start
781
+
782
+ ```bash
783
+ # Check current directory
784
+ thailint pipeline .
785
+
786
+ # Check specific directory
787
+ thailint pipeline src/
788
+
789
+ # Only flag patterns with 2+ filter conditions
790
+ thailint pipeline --min-continues 2 src/
791
+
792
+ # JSON output
793
+ thailint pipeline --format json src/
794
+ ```
795
+
796
+ ### Configuration
797
+
798
+ ```yaml
799
+ # .thailint.yaml
800
+ collection-pipeline:
801
+ enabled: true
802
+ min_continues: 1 # Minimum if/continue patterns to flag
803
+ ignore:
804
+ - "tests/**"
805
+ - "**/legacy/**"
806
+ ```
807
+
808
+ ### Example Violation
809
+
810
+ **Detected Pattern:**
811
+ ```python
812
+ def process_files(paths):
813
+ for path in paths:
814
+ if not path.is_file():
815
+ continue
816
+ analyze(path)
817
+ ```
818
+
819
+ **Violation Message:**
820
+ ```
821
+ src/processor.py:3 - For loop over 'paths' has embedded filtering.
822
+ Consider using a generator expression:
823
+ for path in (path for path in paths if path.is_file()):
824
+ ```
825
+
826
+ **Refactored Code:**
827
+ ```python
828
+ def process_files(paths):
829
+ valid_paths = (p for p in paths if p.is_file())
830
+ for path in valid_paths:
831
+ analyze(path)
832
+ ```
833
+
834
+ ### Why This Matters
835
+
836
+ - **Separation of concerns**: Filtering logic is separate from processing logic
837
+ - **Readability**: Intent is clear at a glance
838
+ - **Testability**: Filtering can be tested independently
839
+ - **Based on**: Martin Fowler's "Replace Loop with Pipeline" refactoring
840
+
841
+ ### Ignoring Violations
842
+
843
+ ```python
844
+ # Line-level ignore
845
+ for item in items: # thailint: ignore[collection-pipeline]
846
+ if not item.valid:
847
+ continue
848
+ process(item)
849
+ ```
850
+
851
+ See [Collection Pipeline Linter Guide](docs/collection-pipeline-linter.md) for comprehensive documentation and refactoring patterns.
852
+
744
853
  ## Magic Numbers Linter
745
854
 
746
855
  ### Overview
@@ -1351,6 +1460,9 @@ docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src/file1.p
1351
1460
  # Lint specific subdirectory
1352
1461
  docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src
1353
1462
 
1463
+ # Collection pipeline linter
1464
+ docker run --rm -v $(pwd):/data washad/thailint:latest pipeline /data/src
1465
+
1354
1466
  # With custom config
1355
1467
  docker run --rm -v $(pwd):/data \
1356
1468
  washad/thailint:latest nesting --config /data/.thailint.yaml /data
@@ -1414,6 +1526,7 @@ docker run --rm -v /path/to/workspace:/workspace \
1414
1526
  - **[Nesting Depth Linter](https://thai-lint.readthedocs.io/en/latest/nesting-linter/)** - Nesting depth analysis guide
1415
1527
  - **[SRP Linter](https://thai-lint.readthedocs.io/en/latest/srp-linter/)** - Single Responsibility Principle guide
1416
1528
  - **[DRY Linter](https://thai-lint.readthedocs.io/en/latest/dry-linter/)** - Duplicate code detection guide
1529
+ - **[Collection Pipeline Linter](https://thai-lint.readthedocs.io/en/latest/collection-pipeline-linter/)** - Loop filtering refactoring guide
1417
1530
  - **[Method Property Linter](https://thai-lint.readthedocs.io/en/latest/method-property-linter/)** - Method-to-property conversion guide
1418
1531
  - **[Stateless Class Linter](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** - Stateless class detection guide
1419
1532
  - **[Pre-commit Hooks](https://thai-lint.readthedocs.io/en/latest/pre-commit-hooks/)** - Automated quality checks