thailint 0.9.0__tar.gz → 0.10.0__tar.gz

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 (118) hide show
  1. {thailint-0.9.0 → thailint-0.10.0}/PKG-INFO +116 -3
  2. {thailint-0.9.0 → thailint-0.10.0}/README.md +115 -2
  3. {thailint-0.9.0 → thailint-0.10.0}/pyproject.toml +1 -1
  4. {thailint-0.9.0 → thailint-0.10.0}/src/cli.py +127 -0
  5. {thailint-0.9.0 → thailint-0.10.0}/src/config.py +2 -3
  6. {thailint-0.9.0 → thailint-0.10.0}/src/core/rule_discovery.py +43 -10
  7. thailint-0.10.0/src/linters/collection_pipeline/__init__.py +90 -0
  8. thailint-0.10.0/src/linters/collection_pipeline/config.py +63 -0
  9. thailint-0.10.0/src/linters/collection_pipeline/continue_analyzer.py +100 -0
  10. thailint-0.10.0/src/linters/collection_pipeline/detector.py +130 -0
  11. thailint-0.10.0/src/linters/collection_pipeline/linter.py +437 -0
  12. thailint-0.10.0/src/linters/collection_pipeline/suggestion_builder.py +63 -0
  13. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/block_filter.py +2 -8
  14. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/python_analyzer.py +34 -18
  15. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/typescript_analyzer.py +61 -31
  16. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/linter.py +7 -11
  17. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/linter.py +28 -8
  18. {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/heuristics.py +4 -3
  19. {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/linter.py +2 -3
  20. {thailint-0.9.0 → thailint-0.10.0}/CHANGELOG.md +0 -0
  21. {thailint-0.9.0 → thailint-0.10.0}/LICENSE +0 -0
  22. {thailint-0.9.0 → thailint-0.10.0}/src/__init__.py +0 -0
  23. {thailint-0.9.0 → thailint-0.10.0}/src/analyzers/__init__.py +0 -0
  24. {thailint-0.9.0 → thailint-0.10.0}/src/analyzers/typescript_base.py +0 -0
  25. {thailint-0.9.0 → thailint-0.10.0}/src/api.py +0 -0
  26. {thailint-0.9.0 → thailint-0.10.0}/src/core/__init__.py +0 -0
  27. {thailint-0.9.0 → thailint-0.10.0}/src/core/base.py +0 -0
  28. {thailint-0.9.0 → thailint-0.10.0}/src/core/cli_utils.py +0 -0
  29. {thailint-0.9.0 → thailint-0.10.0}/src/core/config_parser.py +0 -0
  30. {thailint-0.9.0 → thailint-0.10.0}/src/core/linter_utils.py +0 -0
  31. {thailint-0.9.0 → thailint-0.10.0}/src/core/registry.py +0 -0
  32. {thailint-0.9.0 → thailint-0.10.0}/src/core/types.py +0 -0
  33. {thailint-0.9.0 → thailint-0.10.0}/src/core/violation_builder.py +0 -0
  34. {thailint-0.9.0 → thailint-0.10.0}/src/formatters/__init__.py +0 -0
  35. {thailint-0.9.0 → thailint-0.10.0}/src/formatters/sarif.py +0 -0
  36. {thailint-0.9.0 → thailint-0.10.0}/src/linter_config/__init__.py +0 -0
  37. {thailint-0.9.0 → thailint-0.10.0}/src/linter_config/ignore.py +0 -0
  38. {thailint-0.9.0 → thailint-0.10.0}/src/linter_config/loader.py +0 -0
  39. {thailint-0.9.0 → thailint-0.10.0}/src/linters/__init__.py +0 -0
  40. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/__init__.py +0 -0
  41. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/base_token_analyzer.py +0 -0
  42. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/block_grouper.py +0 -0
  43. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/cache.py +0 -0
  44. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/cache_query.py +0 -0
  45. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/config.py +0 -0
  46. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/config_loader.py +0 -0
  47. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/deduplicator.py +0 -0
  48. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/duplicate_storage.py +0 -0
  49. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/file_analyzer.py +0 -0
  50. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/inline_ignore.py +0 -0
  51. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/linter.py +0 -0
  52. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/storage_initializer.py +0 -0
  53. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/token_hasher.py +0 -0
  54. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/violation_builder.py +0 -0
  55. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/violation_filter.py +0 -0
  56. {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/violation_generator.py +0 -0
  57. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/__init__.py +0 -0
  58. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/atemporal_detector.py +0 -0
  59. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/base_parser.py +0 -0
  60. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/bash_parser.py +0 -0
  61. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/config.py +0 -0
  62. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/css_parser.py +0 -0
  63. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/field_validator.py +0 -0
  64. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/markdown_parser.py +0 -0
  65. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/python_parser.py +0 -0
  66. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/typescript_parser.py +0 -0
  67. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/violation_builder.py +0 -0
  68. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/__init__.py +0 -0
  69. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/config_loader.py +0 -0
  70. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/directory_matcher.py +0 -0
  71. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/path_resolver.py +0 -0
  72. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/pattern_matcher.py +0 -0
  73. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/pattern_validator.py +0 -0
  74. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/rule_checker.py +0 -0
  75. {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/violation_factory.py +0 -0
  76. {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/__init__.py +0 -0
  77. {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/config.py +0 -0
  78. {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/context_analyzer.py +0 -0
  79. {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/linter.py +0 -0
  80. {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/python_analyzer.py +0 -0
  81. {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
  82. {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/violation_builder.py +0 -0
  83. {thailint-0.9.0 → thailint-0.10.0}/src/linters/method_property/__init__.py +0 -0
  84. {thailint-0.9.0 → thailint-0.10.0}/src/linters/method_property/config.py +0 -0
  85. {thailint-0.9.0 → thailint-0.10.0}/src/linters/method_property/linter.py +0 -0
  86. {thailint-0.9.0 → thailint-0.10.0}/src/linters/method_property/python_analyzer.py +0 -0
  87. {thailint-0.9.0 → thailint-0.10.0}/src/linters/method_property/violation_builder.py +0 -0
  88. {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/__init__.py +0 -0
  89. {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/config.py +0 -0
  90. {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/linter.py +0 -0
  91. {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/python_analyzer.py +0 -0
  92. {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/typescript_analyzer.py +0 -0
  93. {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/typescript_function_extractor.py +0 -0
  94. {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/violation_builder.py +0 -0
  95. {thailint-0.9.0 → thailint-0.10.0}/src/linters/print_statements/__init__.py +0 -0
  96. {thailint-0.9.0 → thailint-0.10.0}/src/linters/print_statements/config.py +0 -0
  97. {thailint-0.9.0 → thailint-0.10.0}/src/linters/print_statements/linter.py +0 -0
  98. {thailint-0.9.0 → thailint-0.10.0}/src/linters/print_statements/python_analyzer.py +0 -0
  99. {thailint-0.9.0 → thailint-0.10.0}/src/linters/print_statements/typescript_analyzer.py +0 -0
  100. {thailint-0.9.0 → thailint-0.10.0}/src/linters/print_statements/violation_builder.py +0 -0
  101. {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/__init__.py +0 -0
  102. {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/class_analyzer.py +0 -0
  103. {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/config.py +0 -0
  104. {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/metrics_evaluator.py +0 -0
  105. {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/python_analyzer.py +0 -0
  106. {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/typescript_analyzer.py +0 -0
  107. {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/typescript_metrics_calculator.py +0 -0
  108. {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/violation_builder.py +0 -0
  109. {thailint-0.9.0 → thailint-0.10.0}/src/linters/stateless_class/__init__.py +0 -0
  110. {thailint-0.9.0 → thailint-0.10.0}/src/linters/stateless_class/config.py +0 -0
  111. {thailint-0.9.0 → thailint-0.10.0}/src/linters/stateless_class/linter.py +0 -0
  112. {thailint-0.9.0 → thailint-0.10.0}/src/linters/stateless_class/python_analyzer.py +0 -0
  113. {thailint-0.9.0 → thailint-0.10.0}/src/orchestrator/__init__.py +0 -0
  114. {thailint-0.9.0 → thailint-0.10.0}/src/orchestrator/core.py +0 -0
  115. {thailint-0.9.0 → thailint-0.10.0}/src/orchestrator/language_detector.py +0 -0
  116. {thailint-0.9.0 → thailint-0.10.0}/src/templates/thailint_config_template.yaml +0 -0
  117. {thailint-0.9.0 → thailint-0.10.0}/src/utils/__init__.py +0 -0
  118. {thailint-0.9.0 → thailint-0.10.0}/src/utils/project_root.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thailint
3
- Version: 0.9.0
3
+ Version: 0.10.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-795%2F795%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
@@ -2,8 +2,8 @@
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
5
- [![Tests](https://img.shields.io/badge/tests-728%2F728%20passing-brightgreen.svg)](tests/)
6
- [![Coverage](https://img.shields.io/badge/coverage-87%25-brightgreen.svg)](htmlcov/)
5
+ [![Tests](https://img.shields.io/badge/tests-795%2F795%20passing-brightgreen.svg)](tests/)
6
+ [![Coverage](https://img.shields.io/badge/coverage-88%25-brightgreen.svg)](htmlcov/)
7
7
  [![Documentation Status](https://readthedocs.org/projects/thai-lint/badge/?version=latest)](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
8
8
  [![SARIF 2.1.0](https://img.shields.io/badge/SARIF-2.1.0-orange.svg)](docs/sarif-output.md)
9
9
 
@@ -63,6 +63,12 @@ thailint complements your existing linting stack by catching the patterns AI too
63
63
  - Configurable thresholds (lines, tokens, occurrences)
64
64
  - Language-specific detection (Python, TypeScript, JavaScript)
65
65
  - False positive filtering (keyword args, imports)
66
+ - **Collection Pipeline Linting** - Detect for loops with embedded filtering
67
+ - Based on Martin Fowler's "Replace Loop with Pipeline" refactoring
68
+ - Detects if/continue patterns that should use generator expressions
69
+ - Generates refactoring suggestions with generator syntax
70
+ - Configurable threshold (min_continues)
71
+ - Python support with AST analysis
66
72
  - **Method Property Linting** - Detect methods that should be @property decorators
67
73
  - Python AST-based detection
68
74
  - get_* prefix detection (Java-style getters)
@@ -706,6 +712,109 @@ Built-in filters automatically exclude common non-duplication patterns:
706
712
 
707
713
  See [DRY Linter Guide](https://thai-lint.readthedocs.io/en/latest/dry-linter/) for comprehensive documentation, storage modes, and refactoring patterns.
708
714
 
715
+ ## Collection Pipeline Linter
716
+
717
+ ### Overview
718
+
719
+ 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.
720
+
721
+ ### The Anti-Pattern
722
+
723
+ ```python
724
+ # Anti-pattern: Embedded filtering in loop body
725
+ for file_path in dir_path.glob(pattern):
726
+ if not file_path.is_file():
727
+ continue
728
+ if ignore_parser.is_ignored(file_path):
729
+ continue
730
+ violations.extend(lint_file(file_path))
731
+ ```
732
+
733
+ ### The Solution
734
+
735
+ ```python
736
+ # Collection pipeline: Filtering separated from processing
737
+ valid_files = (
738
+ f for f in dir_path.glob(pattern)
739
+ if f.is_file() and not ignore_parser.is_ignored(f)
740
+ )
741
+ for file_path in valid_files:
742
+ violations.extend(lint_file(file_path))
743
+ ```
744
+
745
+ ### Quick Start
746
+
747
+ ```bash
748
+ # Check current directory
749
+ thailint pipeline .
750
+
751
+ # Check specific directory
752
+ thailint pipeline src/
753
+
754
+ # Only flag patterns with 2+ filter conditions
755
+ thailint pipeline --min-continues 2 src/
756
+
757
+ # JSON output
758
+ thailint pipeline --format json src/
759
+ ```
760
+
761
+ ### Configuration
762
+
763
+ ```yaml
764
+ # .thailint.yaml
765
+ collection-pipeline:
766
+ enabled: true
767
+ min_continues: 1 # Minimum if/continue patterns to flag
768
+ ignore:
769
+ - "tests/**"
770
+ - "**/legacy/**"
771
+ ```
772
+
773
+ ### Example Violation
774
+
775
+ **Detected Pattern:**
776
+ ```python
777
+ def process_files(paths):
778
+ for path in paths:
779
+ if not path.is_file():
780
+ continue
781
+ analyze(path)
782
+ ```
783
+
784
+ **Violation Message:**
785
+ ```
786
+ src/processor.py:3 - For loop over 'paths' has embedded filtering.
787
+ Consider using a generator expression:
788
+ for path in (path for path in paths if path.is_file()):
789
+ ```
790
+
791
+ **Refactored Code:**
792
+ ```python
793
+ def process_files(paths):
794
+ valid_paths = (p for p in paths if p.is_file())
795
+ for path in valid_paths:
796
+ analyze(path)
797
+ ```
798
+
799
+ ### Why This Matters
800
+
801
+ - **Separation of concerns**: Filtering logic is separate from processing logic
802
+ - **Readability**: Intent is clear at a glance
803
+ - **Testability**: Filtering can be tested independently
804
+ - **Based on**: Martin Fowler's "Replace Loop with Pipeline" refactoring
805
+
806
+ ### Ignoring Violations
807
+
808
+ ```python
809
+ # Line-level ignore
810
+ for item in items: # thailint: ignore[collection-pipeline]
811
+ if not item.valid:
812
+ continue
813
+ process(item)
814
+ ```
815
+
816
+ See [Collection Pipeline Linter Guide](docs/collection-pipeline-linter.md) for comprehensive documentation and refactoring patterns.
817
+
709
818
  ## Magic Numbers Linter
710
819
 
711
820
  ### Overview
@@ -1316,6 +1425,9 @@ docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src/file1.p
1316
1425
  # Lint specific subdirectory
1317
1426
  docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src
1318
1427
 
1428
+ # Collection pipeline linter
1429
+ docker run --rm -v $(pwd):/data washad/thailint:latest pipeline /data/src
1430
+
1319
1431
  # With custom config
1320
1432
  docker run --rm -v $(pwd):/data \
1321
1433
  washad/thailint:latest nesting --config /data/.thailint.yaml /data
@@ -1379,6 +1491,7 @@ docker run --rm -v /path/to/workspace:/workspace \
1379
1491
  - **[Nesting Depth Linter](https://thai-lint.readthedocs.io/en/latest/nesting-linter/)** - Nesting depth analysis guide
1380
1492
  - **[SRP Linter](https://thai-lint.readthedocs.io/en/latest/srp-linter/)** - Single Responsibility Principle guide
1381
1493
  - **[DRY Linter](https://thai-lint.readthedocs.io/en/latest/dry-linter/)** - Duplicate code detection guide
1494
+ - **[Collection Pipeline Linter](https://thai-lint.readthedocs.io/en/latest/collection-pipeline-linter/)** - Loop filtering refactoring guide
1382
1495
  - **[Method Property Linter](https://thai-lint.readthedocs.io/en/latest/method-property-linter/)** - Method-to-property conversion guide
1383
1496
  - **[Stateless Class Linter](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** - Stateless class detection guide
1384
1497
  - **[Pre-commit Hooks](https://thai-lint.readthedocs.io/en/latest/pre-commit-hooks/)** - Automated quality checks
@@ -17,7 +17,7 @@ build-backend = "poetry.core.masonry.api"
17
17
 
18
18
  [tool.poetry]
19
19
  name = "thailint"
20
- version = "0.9.0"
20
+ version = "0.10.0"
21
21
  description = "The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages"
22
22
  authors = ["Steve Jackson"]
23
23
  license = "MIT"
@@ -2010,5 +2010,132 @@ def _execute_stateless_class_lint( # pylint: disable=too-many-arguments,too-man
2010
2010
  sys.exit(1 if stateless_class_violations else 0)
2011
2011
 
2012
2012
 
2013
+ # Collection Pipeline command helper functions
2014
+
2015
+
2016
+ def _setup_pipeline_orchestrator(
2017
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
2018
+ ):
2019
+ """Set up orchestrator for pipeline command."""
2020
+ from src.orchestrator.core import Orchestrator
2021
+ from src.utils.project_root import get_project_root
2022
+
2023
+ # Use provided project_root or fall back to auto-detection
2024
+ if project_root is None:
2025
+ first_path = path_objs[0] if path_objs else Path.cwd()
2026
+ search_start = first_path if first_path.is_dir() else first_path.parent
2027
+ project_root = get_project_root(search_start)
2028
+
2029
+ orchestrator = Orchestrator(project_root=project_root)
2030
+
2031
+ if config_file:
2032
+ _load_config_file(orchestrator, config_file, verbose)
2033
+
2034
+ return orchestrator
2035
+
2036
+
2037
+ def _apply_pipeline_config_override(orchestrator, min_continues: int | None, verbose: bool):
2038
+ """Apply min_continues override to orchestrator config."""
2039
+ if min_continues is None:
2040
+ return
2041
+
2042
+ if "collection_pipeline" not in orchestrator.config:
2043
+ orchestrator.config["collection_pipeline"] = {}
2044
+
2045
+ orchestrator.config["collection_pipeline"]["min_continues"] = min_continues
2046
+ if verbose:
2047
+ logger.debug(f"Overriding min_continues to {min_continues}")
2048
+
2049
+
2050
+ def _run_pipeline_lint(orchestrator, path_objs: list[Path], recursive: bool):
2051
+ """Execute collection-pipeline lint on files or directories."""
2052
+ all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
2053
+ return [v for v in all_violations if "collection-pipeline" in v.rule_id]
2054
+
2055
+
2056
+ @cli.command("pipeline")
2057
+ @click.argument("paths", nargs=-1, type=click.Path())
2058
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
2059
+ @format_option
2060
+ @click.option("--min-continues", type=int, help="Override min continue guards to flag (default: 1)")
2061
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
2062
+ @click.pass_context
2063
+ def pipeline( # pylint: disable=too-many-arguments,too-many-positional-arguments
2064
+ ctx,
2065
+ paths: tuple[str, ...],
2066
+ config_file: str | None,
2067
+ format: str,
2068
+ min_continues: int | None,
2069
+ recursive: bool,
2070
+ ):
2071
+ """Check for collection pipeline anti-patterns in code.
2072
+
2073
+ Detects for loops with embedded if/continue filtering patterns that could
2074
+ be refactored to use collection pipelines (generator expressions, filter()).
2075
+
2076
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
2077
+
2078
+ Examples:
2079
+
2080
+ \b
2081
+ # Check current directory (all Python files recursively)
2082
+ thai-lint pipeline
2083
+
2084
+ \b
2085
+ # Check specific directory
2086
+ thai-lint pipeline src/
2087
+
2088
+ \b
2089
+ # Check single file
2090
+ thai-lint pipeline src/app.py
2091
+
2092
+ \b
2093
+ # Only flag loops with 2+ continue guards
2094
+ thai-lint pipeline --min-continues 2 src/
2095
+
2096
+ \b
2097
+ # Get JSON output
2098
+ thai-lint pipeline --format json .
2099
+
2100
+ \b
2101
+ # Get SARIF output for CI/CD integration
2102
+ thai-lint pipeline --format sarif src/
2103
+
2104
+ \b
2105
+ # Use custom config file
2106
+ thai-lint pipeline --config .thailint.yaml src/
2107
+ """
2108
+ verbose = ctx.obj.get("verbose", False)
2109
+ project_root = _get_project_root_from_context(ctx)
2110
+
2111
+ if not paths:
2112
+ paths = (".",)
2113
+
2114
+ path_objs = [Path(p) for p in paths]
2115
+
2116
+ try:
2117
+ _execute_pipeline_lint(
2118
+ path_objs, config_file, format, min_continues, recursive, verbose, project_root
2119
+ )
2120
+ except Exception as e:
2121
+ _handle_linting_error(e, verbose)
2122
+
2123
+
2124
+ def _execute_pipeline_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
2125
+ path_objs, config_file, format, min_continues, recursive, verbose, project_root=None
2126
+ ):
2127
+ """Execute collection-pipeline lint."""
2128
+ _validate_paths_exist(path_objs)
2129
+ orchestrator = _setup_pipeline_orchestrator(path_objs, config_file, verbose, project_root)
2130
+ _apply_pipeline_config_override(orchestrator, min_continues, verbose)
2131
+ pipeline_violations = _run_pipeline_lint(orchestrator, path_objs, recursive)
2132
+
2133
+ if verbose:
2134
+ logger.info(f"Found {len(pipeline_violations)} collection-pipeline violation(s)")
2135
+
2136
+ format_violations(pipeline_violations, format)
2137
+ sys.exit(1 if pipeline_violations else 0)
2138
+
2139
+
2013
2140
  if __name__ == "__main__":
2014
2141
  cli()
@@ -103,9 +103,8 @@ def _load_from_explicit_path(config_path: Path) -> dict[str, Any]:
103
103
 
104
104
  def _load_from_default_locations() -> dict[str, Any]:
105
105
  """Load config from default locations."""
106
- for location in CONFIG_LOCATIONS:
107
- if not location.exists():
108
- continue
106
+ existing_locations = (loc for loc in CONFIG_LOCATIONS if loc.exists())
107
+ for location in existing_locations:
109
108
  loaded_config = _try_load_from_location(location)
110
109
  if loaded_config:
111
110
  return loaded_config
@@ -20,6 +20,7 @@ Implementation: Package traversal with pkgutil, class introspection with inspect
20
20
  import importlib
21
21
  import inspect
22
22
  import pkgutil
23
+ from types import ModuleType
23
24
  from typing import Any
24
25
 
25
26
  from .base import BaseLintRule
@@ -87,19 +88,51 @@ def _discover_from_module(module_path: str) -> list[BaseLintRule]:
87
88
  Returns:
88
89
  List of discovered rule instances
89
90
  """
91
+ module = _try_import_module(module_path)
92
+ if module is None:
93
+ return []
94
+ return _extract_rules_from_module(module)
95
+
96
+
97
+ def _try_import_module(module_path: str) -> ModuleType | None:
98
+ """Try to import a module, returning None on failure.
99
+
100
+ Args:
101
+ module_path: Full module path to import
102
+
103
+ Returns:
104
+ Module object or None if import fails
105
+ """
90
106
  try:
91
- module = importlib.import_module(module_path)
107
+ return importlib.import_module(module_path)
92
108
  except (ImportError, AttributeError):
93
- return []
109
+ return None
94
110
 
95
- rules = []
96
- for _name, obj in inspect.getmembers(module):
97
- if not _is_rule_class(obj):
98
- continue
99
- rule_instance = _try_instantiate_rule(obj)
100
- if rule_instance:
101
- rules.append(rule_instance)
102
- return rules
111
+
112
+ def _extract_rules_from_module(module: ModuleType) -> list[BaseLintRule]:
113
+ """Extract rule instances from a module.
114
+
115
+ Args:
116
+ module: Imported module to scan
117
+
118
+ Returns:
119
+ List of discovered rule instances
120
+ """
121
+ rule_classes = [obj for _name, obj in inspect.getmembers(module) if _is_rule_class(obj)]
122
+ return _instantiate_rules(rule_classes)
123
+
124
+
125
+ def _instantiate_rules(rule_classes: list[type[BaseLintRule]]) -> list[BaseLintRule]:
126
+ """Instantiate a list of rule classes.
127
+
128
+ Args:
129
+ rule_classes: List of rule classes to instantiate
130
+
131
+ Returns:
132
+ List of successfully instantiated rules
133
+ """
134
+ instances = (_try_instantiate_rule(cls) for cls in rule_classes)
135
+ return [inst for inst in instances if inst is not None]
103
136
 
104
137
 
105
138
  def _try_instantiate_rule(rule_class: type[BaseLintRule]) -> BaseLintRule | None:
@@ -0,0 +1,90 @@
1
+ """
2
+ Purpose: Collection pipeline linter package initialization
3
+
4
+ Scope: Exports for collection-pipeline linter module
5
+
6
+ Overview: Initializes the collection-pipeline linter package and exposes the main rule class
7
+ for external use. Exports CollectionPipelineRule as the primary interface for the linter,
8
+ allowing the orchestrator to discover and instantiate the rule. Also exports configuration
9
+ and detector classes for advanced use cases. Provides a convenience lint() function for
10
+ direct usage without orchestrator setup. This module serves as the entry point for
11
+ the collection-pipeline linter functionality within the thai-lint framework.
12
+
13
+ Dependencies: CollectionPipelineRule, CollectionPipelineConfig, PipelinePatternDetector
14
+
15
+ Exports: CollectionPipelineRule (primary), CollectionPipelineConfig, PipelinePatternDetector, lint
16
+
17
+ Interfaces: Standard Python package initialization with __all__ for explicit exports
18
+
19
+ Implementation: Simple re-export pattern for package interface, convenience lint function
20
+ """
21
+
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from .config import DEFAULT_MIN_CONTINUES, CollectionPipelineConfig
26
+ from .detector import PatternMatch, PipelinePatternDetector
27
+ from .linter import CollectionPipelineRule
28
+
29
+ __all__ = [
30
+ "CollectionPipelineRule",
31
+ "CollectionPipelineConfig",
32
+ "PipelinePatternDetector",
33
+ "PatternMatch",
34
+ "lint",
35
+ ]
36
+
37
+
38
+ def lint(
39
+ path: Path | str,
40
+ config: dict[str, Any] | None = None,
41
+ min_continues: int = DEFAULT_MIN_CONTINUES,
42
+ ) -> list:
43
+ """Lint a file or directory for collection pipeline violations.
44
+
45
+ Args:
46
+ path: Path to file or directory to lint
47
+ config: Configuration dict (optional, uses defaults if not provided)
48
+ min_continues: Minimum if/continue patterns to flag (default: 1)
49
+
50
+ Returns:
51
+ List of violations found
52
+
53
+ Example:
54
+ >>> from src.linters.collection_pipeline import lint
55
+ >>> violations = lint('src/my_module.py', min_continues=2)
56
+ >>> for v in violations:
57
+ ... print(f"{v.file_path}:{v.line} - {v.message}")
58
+ """
59
+ path_obj = Path(path) if isinstance(path, str) else path
60
+ project_root = path_obj if path_obj.is_dir() else path_obj.parent
61
+
62
+ orchestrator = _setup_pipeline_orchestrator(project_root, config, min_continues)
63
+ violations = _execute_pipeline_lint(orchestrator, path_obj)
64
+
65
+ return [v for v in violations if "collection-pipeline" in v.rule_id]
66
+
67
+
68
+ def _setup_pipeline_orchestrator(
69
+ project_root: Path, config: dict[str, Any] | None, min_continues: int
70
+ ) -> Any:
71
+ """Set up orchestrator with collection-pipeline config."""
72
+ from src.orchestrator.core import Orchestrator
73
+
74
+ orchestrator = Orchestrator(project_root=project_root)
75
+
76
+ if config:
77
+ orchestrator.config["collection-pipeline"] = config
78
+ else:
79
+ orchestrator.config["collection-pipeline"] = {"min_continues": min_continues}
80
+
81
+ return orchestrator
82
+
83
+
84
+ def _execute_pipeline_lint(orchestrator: Any, path_obj: Path) -> list:
85
+ """Execute linting on file or directory."""
86
+ if path_obj.is_file():
87
+ return orchestrator.lint_file(path_obj)
88
+ if path_obj.is_dir():
89
+ return orchestrator.lint_directory(path_obj)
90
+ return []
@@ -0,0 +1,63 @@
1
+ """
2
+ Purpose: Configuration dataclass for collection-pipeline linter
3
+
4
+ Scope: Define configurable options for embedded filtering pattern detection
5
+
6
+ Overview: Provides CollectionPipelineConfig for customizing linter behavior including
7
+ minimum number of continue patterns to flag, enable/disable toggle, and ignore
8
+ patterns. Integrates with the orchestrator's configuration system to allow users
9
+ to customize collection-pipeline detection via .thailint.yaml configuration files.
10
+ Follows the same configuration pattern as other thai-lint linters.
11
+
12
+ Dependencies: dataclasses, typing
13
+
14
+ Exports: CollectionPipelineConfig dataclass, DEFAULT_MIN_CONTINUES constant
15
+
16
+ Interfaces: CollectionPipelineConfig.from_dict() class method for configuration loading
17
+
18
+ Implementation: Dataclass with sensible defaults and config loading from dictionary
19
+ """
20
+
21
+ from dataclasses import dataclass, field
22
+ from typing import Any
23
+
24
+ # Default threshold for minimum continue guards to flag
25
+ DEFAULT_MIN_CONTINUES = 1
26
+
27
+
28
+ @dataclass
29
+ class CollectionPipelineConfig:
30
+ """Configuration for collection-pipeline linter."""
31
+
32
+ enabled: bool = True
33
+ """Whether the linter is enabled."""
34
+
35
+ min_continues: int = DEFAULT_MIN_CONTINUES
36
+ """Minimum number of if/continue patterns required to flag a violation."""
37
+
38
+ ignore: list[str] = field(default_factory=list)
39
+ """File patterns to ignore."""
40
+
41
+ def __post_init__(self) -> None:
42
+ """Validate configuration values."""
43
+ if self.min_continues < 1:
44
+ raise ValueError(f"min_continues must be at least 1, got {self.min_continues}")
45
+
46
+ @classmethod
47
+ def from_dict(
48
+ cls, config: dict[str, Any], language: str | None = None
49
+ ) -> "CollectionPipelineConfig":
50
+ """Load configuration from dictionary.
51
+
52
+ Args:
53
+ config: Dictionary containing configuration values
54
+ language: Programming language (unused, for interface compatibility)
55
+
56
+ Returns:
57
+ CollectionPipelineConfig instance with values from dictionary
58
+ """
59
+ return cls(
60
+ enabled=config.get("enabled", True),
61
+ min_continues=config.get("min_continues", DEFAULT_MIN_CONTINUES),
62
+ ignore=config.get("ignore", []),
63
+ )