thailint 0.8.0__tar.gz → 0.9.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 (114) hide show
  1. {thailint-0.8.0 → thailint-0.9.0}/CHANGELOG.md +13 -0
  2. {thailint-0.8.0 → thailint-0.9.0}/PKG-INFO +112 -2
  3. {thailint-0.8.0 → thailint-0.9.0}/README.md +111 -1
  4. {thailint-0.8.0 → thailint-0.9.0}/pyproject.toml +1 -1
  5. {thailint-0.8.0 → thailint-0.9.0}/src/cli.py +115 -0
  6. {thailint-0.8.0 → thailint-0.9.0}/src/core/base.py +4 -0
  7. thailint-0.9.0/src/core/rule_discovery.py +158 -0
  8. {thailint-0.8.0 → thailint-0.9.0}/src/core/violation_builder.py +75 -15
  9. {thailint-0.8.0 → thailint-0.9.0}/src/linter_config/loader.py +43 -11
  10. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/block_filter.py +4 -0
  11. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/block_grouper.py +4 -0
  12. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/cache_query.py +4 -0
  13. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/token_hasher.py +5 -1
  14. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/violation_builder.py +4 -0
  15. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/violation_filter.py +4 -0
  16. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_header/bash_parser.py +4 -0
  17. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_placement/directory_matcher.py +4 -0
  18. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_placement/pattern_matcher.py +4 -0
  19. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_placement/pattern_validator.py +4 -0
  20. {thailint-0.8.0 → thailint-0.9.0}/src/linters/magic_numbers/context_analyzer.py +4 -0
  21. {thailint-0.8.0 → thailint-0.9.0}/src/linters/magic_numbers/typescript_analyzer.py +4 -0
  22. {thailint-0.8.0 → thailint-0.9.0}/src/linters/nesting/python_analyzer.py +4 -0
  23. {thailint-0.8.0 → thailint-0.9.0}/src/linters/nesting/typescript_function_extractor.py +4 -0
  24. {thailint-0.8.0 → thailint-0.9.0}/src/linters/print_statements/typescript_analyzer.py +4 -0
  25. {thailint-0.8.0 → thailint-0.9.0}/src/linters/srp/class_analyzer.py +4 -0
  26. {thailint-0.8.0 → thailint-0.9.0}/src/linters/srp/python_analyzer.py +55 -20
  27. thailint-0.9.0/src/linters/srp/typescript_metrics_calculator.py +126 -0
  28. {thailint-0.8.0 → thailint-0.9.0}/src/linters/srp/violation_builder.py +4 -0
  29. thailint-0.9.0/src/linters/stateless_class/__init__.py +25 -0
  30. thailint-0.9.0/src/linters/stateless_class/config.py +58 -0
  31. thailint-0.9.0/src/linters/stateless_class/linter.py +355 -0
  32. thailint-0.9.0/src/linters/stateless_class/python_analyzer.py +299 -0
  33. thailint-0.8.0/src/core/rule_discovery.py +0 -132
  34. thailint-0.8.0/src/linters/srp/typescript_metrics_calculator.py +0 -90
  35. {thailint-0.8.0 → thailint-0.9.0}/LICENSE +0 -0
  36. {thailint-0.8.0 → thailint-0.9.0}/src/__init__.py +0 -0
  37. {thailint-0.8.0 → thailint-0.9.0}/src/analyzers/__init__.py +0 -0
  38. {thailint-0.8.0 → thailint-0.9.0}/src/analyzers/typescript_base.py +0 -0
  39. {thailint-0.8.0 → thailint-0.9.0}/src/api.py +0 -0
  40. {thailint-0.8.0 → thailint-0.9.0}/src/config.py +0 -0
  41. {thailint-0.8.0 → thailint-0.9.0}/src/core/__init__.py +0 -0
  42. {thailint-0.8.0 → thailint-0.9.0}/src/core/cli_utils.py +0 -0
  43. {thailint-0.8.0 → thailint-0.9.0}/src/core/config_parser.py +0 -0
  44. {thailint-0.8.0 → thailint-0.9.0}/src/core/linter_utils.py +0 -0
  45. {thailint-0.8.0 → thailint-0.9.0}/src/core/registry.py +0 -0
  46. {thailint-0.8.0 → thailint-0.9.0}/src/core/types.py +0 -0
  47. {thailint-0.8.0 → thailint-0.9.0}/src/formatters/__init__.py +0 -0
  48. {thailint-0.8.0 → thailint-0.9.0}/src/formatters/sarif.py +0 -0
  49. {thailint-0.8.0 → thailint-0.9.0}/src/linter_config/__init__.py +0 -0
  50. {thailint-0.8.0 → thailint-0.9.0}/src/linter_config/ignore.py +0 -0
  51. {thailint-0.8.0 → thailint-0.9.0}/src/linters/__init__.py +0 -0
  52. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/__init__.py +0 -0
  53. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/base_token_analyzer.py +0 -0
  54. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/cache.py +0 -0
  55. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/config.py +0 -0
  56. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/config_loader.py +0 -0
  57. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/deduplicator.py +0 -0
  58. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/duplicate_storage.py +0 -0
  59. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/file_analyzer.py +0 -0
  60. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/inline_ignore.py +0 -0
  61. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/linter.py +0 -0
  62. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/python_analyzer.py +0 -0
  63. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/storage_initializer.py +0 -0
  64. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/typescript_analyzer.py +0 -0
  65. {thailint-0.8.0 → thailint-0.9.0}/src/linters/dry/violation_generator.py +0 -0
  66. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_header/__init__.py +0 -0
  67. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_header/atemporal_detector.py +0 -0
  68. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_header/base_parser.py +0 -0
  69. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_header/config.py +0 -0
  70. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_header/css_parser.py +0 -0
  71. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_header/field_validator.py +0 -0
  72. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_header/linter.py +0 -0
  73. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_header/markdown_parser.py +0 -0
  74. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_header/python_parser.py +0 -0
  75. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_header/typescript_parser.py +0 -0
  76. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_header/violation_builder.py +0 -0
  77. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_placement/__init__.py +0 -0
  78. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_placement/config_loader.py +0 -0
  79. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_placement/linter.py +0 -0
  80. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_placement/path_resolver.py +0 -0
  81. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_placement/rule_checker.py +0 -0
  82. {thailint-0.8.0 → thailint-0.9.0}/src/linters/file_placement/violation_factory.py +0 -0
  83. {thailint-0.8.0 → thailint-0.9.0}/src/linters/magic_numbers/__init__.py +0 -0
  84. {thailint-0.8.0 → thailint-0.9.0}/src/linters/magic_numbers/config.py +0 -0
  85. {thailint-0.8.0 → thailint-0.9.0}/src/linters/magic_numbers/linter.py +0 -0
  86. {thailint-0.8.0 → thailint-0.9.0}/src/linters/magic_numbers/python_analyzer.py +0 -0
  87. {thailint-0.8.0 → thailint-0.9.0}/src/linters/magic_numbers/violation_builder.py +0 -0
  88. {thailint-0.8.0 → thailint-0.9.0}/src/linters/method_property/__init__.py +0 -0
  89. {thailint-0.8.0 → thailint-0.9.0}/src/linters/method_property/config.py +0 -0
  90. {thailint-0.8.0 → thailint-0.9.0}/src/linters/method_property/linter.py +0 -0
  91. {thailint-0.8.0 → thailint-0.9.0}/src/linters/method_property/python_analyzer.py +0 -0
  92. {thailint-0.8.0 → thailint-0.9.0}/src/linters/method_property/violation_builder.py +0 -0
  93. {thailint-0.8.0 → thailint-0.9.0}/src/linters/nesting/__init__.py +0 -0
  94. {thailint-0.8.0 → thailint-0.9.0}/src/linters/nesting/config.py +0 -0
  95. {thailint-0.8.0 → thailint-0.9.0}/src/linters/nesting/linter.py +0 -0
  96. {thailint-0.8.0 → thailint-0.9.0}/src/linters/nesting/typescript_analyzer.py +0 -0
  97. {thailint-0.8.0 → thailint-0.9.0}/src/linters/nesting/violation_builder.py +0 -0
  98. {thailint-0.8.0 → thailint-0.9.0}/src/linters/print_statements/__init__.py +0 -0
  99. {thailint-0.8.0 → thailint-0.9.0}/src/linters/print_statements/config.py +0 -0
  100. {thailint-0.8.0 → thailint-0.9.0}/src/linters/print_statements/linter.py +0 -0
  101. {thailint-0.8.0 → thailint-0.9.0}/src/linters/print_statements/python_analyzer.py +0 -0
  102. {thailint-0.8.0 → thailint-0.9.0}/src/linters/print_statements/violation_builder.py +0 -0
  103. {thailint-0.8.0 → thailint-0.9.0}/src/linters/srp/__init__.py +0 -0
  104. {thailint-0.8.0 → thailint-0.9.0}/src/linters/srp/config.py +0 -0
  105. {thailint-0.8.0 → thailint-0.9.0}/src/linters/srp/heuristics.py +0 -0
  106. {thailint-0.8.0 → thailint-0.9.0}/src/linters/srp/linter.py +0 -0
  107. {thailint-0.8.0 → thailint-0.9.0}/src/linters/srp/metrics_evaluator.py +0 -0
  108. {thailint-0.8.0 → thailint-0.9.0}/src/linters/srp/typescript_analyzer.py +0 -0
  109. {thailint-0.8.0 → thailint-0.9.0}/src/orchestrator/__init__.py +0 -0
  110. {thailint-0.8.0 → thailint-0.9.0}/src/orchestrator/core.py +0 -0
  111. {thailint-0.8.0 → thailint-0.9.0}/src/orchestrator/language_detector.py +0 -0
  112. {thailint-0.8.0 → thailint-0.9.0}/src/templates/thailint_config_template.yaml +0 -0
  113. {thailint-0.8.0 → thailint-0.9.0}/src/utils/__init__.py +0 -0
  114. {thailint-0.8.0 → thailint-0.9.0}/src/utils/project_root.py +0 -0
@@ -26,6 +26,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
26
26
 
27
27
  ### Added
28
28
 
29
+ - **Stateless Class Linter** - Detect Python classes without state that should be module-level functions
30
+ - AST-based detection of classes without `__init__`/`__new__` constructors
31
+ - Detects classes without instance state (`self.attr` assignments)
32
+ - Excludes ABC, Protocol, and decorated classes (legitimate patterns)
33
+ - Excludes classes with class-level attributes
34
+ - Minimum 2 methods required to flag (avoids false positives on simple wrappers)
35
+ - CLI command: `thailint stateless-class src/`
36
+ - JSON and SARIF output formats
37
+ - Configuration via `.thailint.yaml` with `min_methods` and `ignore` options
38
+ - Self-dogfooded: 23 violations in thai-lint codebase were fixed
39
+ - 28 tests (15 detector + 13 CLI) with 100% pass rate
40
+ - Documentation: `docs/stateless-class-linter.md`
41
+
29
42
  - **Project Root Detection System** - Three-level precedence system for accurate configuration and ignore pattern resolution
30
43
  - `--project-root` CLI option for explicit project root specification (highest priority)
31
44
  - Automatic project root inference from `--config` path (config's parent directory becomes project root)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thailint
3
- Version: 0.8.0
3
+ Version: 0.9.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,7 +37,7 @@ 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-682%2F682%20passing-brightgreen.svg)](tests/)
40
+ [![Tests](https://img.shields.io/badge/tests-728%2F728%20passing-brightgreen.svg)](tests/)
41
41
  [![Coverage](https://img.shields.io/badge/coverage-87%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)
@@ -104,6 +104,12 @@ thailint complements your existing linting stack by catching the patterns AI too
104
104
  - Simple computed value detection
105
105
  - Action verb exclusion (to_*, finalize, serialize)
106
106
  - Test file detection
107
+ - **Stateless Class Linting** - Detect classes that should be module-level functions
108
+ - Python AST-based detection
109
+ - No constructor (__init__ or __new__) detection
110
+ - No instance state (self.attr) detection
111
+ - Excludes ABC, Protocol, and decorated classes
112
+ - Helpful refactoring suggestions
107
113
  - **Pluggable Architecture** - Easy to extend with custom linters
108
114
  - **Multi-Language Support** - Python, TypeScript, JavaScript, and more
109
115
  - **Flexible Configuration** - YAML/JSON configs with pattern matching
@@ -1017,6 +1023,108 @@ Overview: Created 2024-01-15. # thailint: ignore[file-header]
1017
1023
 
1018
1024
  See **[How to Ignore Violations](https://thai-lint.readthedocs.io/en/latest/how-to-ignore-violations/)** and **[File Header Linter Guide](https://thai-lint.readthedocs.io/en/latest/file-header-linter/)** for complete documentation.
1019
1025
 
1026
+ ## Stateless Class Linter
1027
+
1028
+ ### Overview
1029
+
1030
+ The stateless class linter detects Python classes that have no state (no constructor, no instance attributes) and should be refactored to module-level functions. This is a common anti-pattern in AI-generated code.
1031
+
1032
+ ### What Are Stateless Classes?
1033
+
1034
+ Stateless classes are classes that:
1035
+ - Have no `__init__` or `__new__` method
1036
+ - Have no instance attributes (`self.attr` assignments)
1037
+ - Have 2+ methods (grouped functionality without state)
1038
+
1039
+ ```python
1040
+ # Bad - Stateless class (no state, just grouped functions)
1041
+ class TokenHasher:
1042
+ def hash_token(self, token: str) -> str:
1043
+ return hashlib.sha256(token.encode()).hexdigest()
1044
+
1045
+ def verify_token(self, token: str, hash_value: str) -> bool:
1046
+ return self.hash_token(token) == hash_value
1047
+
1048
+ # Good - Module-level functions
1049
+ def hash_token(token: str) -> str:
1050
+ return hashlib.sha256(token.encode()).hexdigest()
1051
+
1052
+ def verify_token(token: str, hash_value: str) -> bool:
1053
+ return hash_token(token) == hash_value
1054
+ ```
1055
+
1056
+ ### Quick Start
1057
+
1058
+ ```bash
1059
+ # Check stateless classes in current directory
1060
+ thailint stateless-class .
1061
+
1062
+ # Check specific directory
1063
+ thailint stateless-class src/
1064
+
1065
+ # Get JSON output
1066
+ thailint stateless-class --format json src/
1067
+ ```
1068
+
1069
+ ### Configuration
1070
+
1071
+ Add to `.thailint.yaml`:
1072
+
1073
+ ```yaml
1074
+ stateless-class:
1075
+ enabled: true
1076
+ min_methods: 2 # Minimum methods to flag
1077
+ ```
1078
+
1079
+ ### Example Violation
1080
+
1081
+ **Code with stateless class:**
1082
+ ```python
1083
+ class StringUtils:
1084
+ def capitalize_words(self, text: str) -> str:
1085
+ return ' '.join(w.capitalize() for w in text.split())
1086
+
1087
+ def reverse_words(self, text: str) -> str:
1088
+ return ' '.join(reversed(text.split()))
1089
+ ```
1090
+
1091
+ **Violation message:**
1092
+ ```
1093
+ src/utils.py:1 - Class 'StringUtils' has no state and should be refactored to module-level functions
1094
+ ```
1095
+
1096
+ **Refactored code:**
1097
+ ```python
1098
+ def capitalize_words(text: str) -> str:
1099
+ return ' '.join(w.capitalize() for w in text.split())
1100
+
1101
+ def reverse_words(text: str) -> str:
1102
+ return ' '.join(reversed(text.split()))
1103
+ ```
1104
+
1105
+ ### Exclusion Rules
1106
+
1107
+ The linter does NOT flag classes that:
1108
+ - Have `__init__` or `__new__` constructors
1109
+ - Have instance attributes (`self.attr = value`)
1110
+ - Have class-level attributes
1111
+ - Inherit from ABC or Protocol
1112
+ - Have any decorator (`@dataclass`, `@register`, etc.)
1113
+ - Have 0-1 methods
1114
+
1115
+ ### Ignoring Violations
1116
+
1117
+ ```python
1118
+ # Line-level ignore
1119
+ class TokenHasher: # thailint: ignore[stateless-class] - Legacy API
1120
+ def hash(self, token): ...
1121
+
1122
+ # File-level ignore
1123
+ # thailint: ignore-file[stateless-class]
1124
+ ```
1125
+
1126
+ See **[How to Ignore Violations](https://thai-lint.readthedocs.io/en/latest/how-to-ignore-violations/)** and **[Stateless Class Linter Guide](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** for complete documentation.
1127
+
1020
1128
  ## Pre-commit Hooks
1021
1129
 
1022
1130
  Automate code quality checks before every commit and push with pre-commit hooks.
@@ -1306,6 +1414,8 @@ docker run --rm -v /path/to/workspace:/workspace \
1306
1414
  - **[Nesting Depth Linter](https://thai-lint.readthedocs.io/en/latest/nesting-linter/)** - Nesting depth analysis guide
1307
1415
  - **[SRP Linter](https://thai-lint.readthedocs.io/en/latest/srp-linter/)** - Single Responsibility Principle guide
1308
1416
  - **[DRY Linter](https://thai-lint.readthedocs.io/en/latest/dry-linter/)** - Duplicate code detection guide
1417
+ - **[Method Property Linter](https://thai-lint.readthedocs.io/en/latest/method-property-linter/)** - Method-to-property conversion guide
1418
+ - **[Stateless Class Linter](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** - Stateless class detection guide
1309
1419
  - **[Pre-commit Hooks](https://thai-lint.readthedocs.io/en/latest/pre-commit-hooks/)** - Automated quality checks
1310
1420
  - **[SARIF Output Guide](docs/sarif-output.md)** - SARIF format for GitHub Code Scanning and CI/CD
1311
1421
  - **[Publishing Guide](https://thai-lint.readthedocs.io/en/latest/releasing/)** - Release and publishing workflow
@@ -2,7 +2,7 @@
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-682%2F682%20passing-brightgreen.svg)](tests/)
5
+ [![Tests](https://img.shields.io/badge/tests-728%2F728%20passing-brightgreen.svg)](tests/)
6
6
  [![Coverage](https://img.shields.io/badge/coverage-87%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)
@@ -69,6 +69,12 @@ thailint complements your existing linting stack by catching the patterns AI too
69
69
  - Simple computed value detection
70
70
  - Action verb exclusion (to_*, finalize, serialize)
71
71
  - Test file detection
72
+ - **Stateless Class Linting** - Detect classes that should be module-level functions
73
+ - Python AST-based detection
74
+ - No constructor (__init__ or __new__) detection
75
+ - No instance state (self.attr) detection
76
+ - Excludes ABC, Protocol, and decorated classes
77
+ - Helpful refactoring suggestions
72
78
  - **Pluggable Architecture** - Easy to extend with custom linters
73
79
  - **Multi-Language Support** - Python, TypeScript, JavaScript, and more
74
80
  - **Flexible Configuration** - YAML/JSON configs with pattern matching
@@ -982,6 +988,108 @@ Overview: Created 2024-01-15. # thailint: ignore[file-header]
982
988
 
983
989
  See **[How to Ignore Violations](https://thai-lint.readthedocs.io/en/latest/how-to-ignore-violations/)** and **[File Header Linter Guide](https://thai-lint.readthedocs.io/en/latest/file-header-linter/)** for complete documentation.
984
990
 
991
+ ## Stateless Class Linter
992
+
993
+ ### Overview
994
+
995
+ The stateless class linter detects Python classes that have no state (no constructor, no instance attributes) and should be refactored to module-level functions. This is a common anti-pattern in AI-generated code.
996
+
997
+ ### What Are Stateless Classes?
998
+
999
+ Stateless classes are classes that:
1000
+ - Have no `__init__` or `__new__` method
1001
+ - Have no instance attributes (`self.attr` assignments)
1002
+ - Have 2+ methods (grouped functionality without state)
1003
+
1004
+ ```python
1005
+ # Bad - Stateless class (no state, just grouped functions)
1006
+ class TokenHasher:
1007
+ def hash_token(self, token: str) -> str:
1008
+ return hashlib.sha256(token.encode()).hexdigest()
1009
+
1010
+ def verify_token(self, token: str, hash_value: str) -> bool:
1011
+ return self.hash_token(token) == hash_value
1012
+
1013
+ # Good - Module-level functions
1014
+ def hash_token(token: str) -> str:
1015
+ return hashlib.sha256(token.encode()).hexdigest()
1016
+
1017
+ def verify_token(token: str, hash_value: str) -> bool:
1018
+ return hash_token(token) == hash_value
1019
+ ```
1020
+
1021
+ ### Quick Start
1022
+
1023
+ ```bash
1024
+ # Check stateless classes in current directory
1025
+ thailint stateless-class .
1026
+
1027
+ # Check specific directory
1028
+ thailint stateless-class src/
1029
+
1030
+ # Get JSON output
1031
+ thailint stateless-class --format json src/
1032
+ ```
1033
+
1034
+ ### Configuration
1035
+
1036
+ Add to `.thailint.yaml`:
1037
+
1038
+ ```yaml
1039
+ stateless-class:
1040
+ enabled: true
1041
+ min_methods: 2 # Minimum methods to flag
1042
+ ```
1043
+
1044
+ ### Example Violation
1045
+
1046
+ **Code with stateless class:**
1047
+ ```python
1048
+ class StringUtils:
1049
+ def capitalize_words(self, text: str) -> str:
1050
+ return ' '.join(w.capitalize() for w in text.split())
1051
+
1052
+ def reverse_words(self, text: str) -> str:
1053
+ return ' '.join(reversed(text.split()))
1054
+ ```
1055
+
1056
+ **Violation message:**
1057
+ ```
1058
+ src/utils.py:1 - Class 'StringUtils' has no state and should be refactored to module-level functions
1059
+ ```
1060
+
1061
+ **Refactored code:**
1062
+ ```python
1063
+ def capitalize_words(text: str) -> str:
1064
+ return ' '.join(w.capitalize() for w in text.split())
1065
+
1066
+ def reverse_words(text: str) -> str:
1067
+ return ' '.join(reversed(text.split()))
1068
+ ```
1069
+
1070
+ ### Exclusion Rules
1071
+
1072
+ The linter does NOT flag classes that:
1073
+ - Have `__init__` or `__new__` constructors
1074
+ - Have instance attributes (`self.attr = value`)
1075
+ - Have class-level attributes
1076
+ - Inherit from ABC or Protocol
1077
+ - Have any decorator (`@dataclass`, `@register`, etc.)
1078
+ - Have 0-1 methods
1079
+
1080
+ ### Ignoring Violations
1081
+
1082
+ ```python
1083
+ # Line-level ignore
1084
+ class TokenHasher: # thailint: ignore[stateless-class] - Legacy API
1085
+ def hash(self, token): ...
1086
+
1087
+ # File-level ignore
1088
+ # thailint: ignore-file[stateless-class]
1089
+ ```
1090
+
1091
+ See **[How to Ignore Violations](https://thai-lint.readthedocs.io/en/latest/how-to-ignore-violations/)** and **[Stateless Class Linter Guide](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** for complete documentation.
1092
+
985
1093
  ## Pre-commit Hooks
986
1094
 
987
1095
  Automate code quality checks before every commit and push with pre-commit hooks.
@@ -1271,6 +1379,8 @@ docker run --rm -v /path/to/workspace:/workspace \
1271
1379
  - **[Nesting Depth Linter](https://thai-lint.readthedocs.io/en/latest/nesting-linter/)** - Nesting depth analysis guide
1272
1380
  - **[SRP Linter](https://thai-lint.readthedocs.io/en/latest/srp-linter/)** - Single Responsibility Principle guide
1273
1381
  - **[DRY Linter](https://thai-lint.readthedocs.io/en/latest/dry-linter/)** - Duplicate code detection guide
1382
+ - **[Method Property Linter](https://thai-lint.readthedocs.io/en/latest/method-property-linter/)** - Method-to-property conversion guide
1383
+ - **[Stateless Class Linter](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** - Stateless class detection guide
1274
1384
  - **[Pre-commit Hooks](https://thai-lint.readthedocs.io/en/latest/pre-commit-hooks/)** - Automated quality checks
1275
1385
  - **[SARIF Output Guide](docs/sarif-output.md)** - SARIF format for GitHub Code Scanning and CI/CD
1276
1386
  - **[Publishing Guide](https://thai-lint.readthedocs.io/en/latest/releasing/)** - Release and publishing workflow
@@ -17,7 +17,7 @@ build-backend = "poetry.core.masonry.api"
17
17
 
18
18
  [tool.poetry]
19
19
  name = "thailint"
20
- version = "0.8.0"
20
+ version = "0.9.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"
@@ -1895,5 +1895,120 @@ def _execute_method_property_lint( # pylint: disable=too-many-arguments,too-man
1895
1895
  sys.exit(1 if method_property_violations else 0)
1896
1896
 
1897
1897
 
1898
+ # =============================================================================
1899
+ # Stateless Class Linter Command
1900
+ # =============================================================================
1901
+
1902
+
1903
+ def _setup_stateless_class_orchestrator(
1904
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
1905
+ ):
1906
+ """Set up orchestrator for stateless-class command."""
1907
+ from src.orchestrator.core import Orchestrator
1908
+ from src.utils.project_root import get_project_root
1909
+
1910
+ if project_root is None:
1911
+ first_path = path_objs[0] if path_objs else Path.cwd()
1912
+ search_start = first_path if first_path.is_dir() else first_path.parent
1913
+ project_root = get_project_root(search_start)
1914
+
1915
+ orchestrator = Orchestrator(project_root=project_root)
1916
+
1917
+ if config_file:
1918
+ _load_config_file(orchestrator, config_file, verbose)
1919
+
1920
+ return orchestrator
1921
+
1922
+
1923
+ def _run_stateless_class_lint(orchestrator, path_objs: list[Path], recursive: bool):
1924
+ """Execute stateless-class lint on files or directories."""
1925
+ all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
1926
+ return [v for v in all_violations if "stateless-class" in v.rule_id]
1927
+
1928
+
1929
+ @cli.command("stateless-class")
1930
+ @click.argument("paths", nargs=-1, type=click.Path())
1931
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
1932
+ @format_option
1933
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
1934
+ @click.pass_context
1935
+ def stateless_class(
1936
+ ctx,
1937
+ paths: tuple[str, ...],
1938
+ config_file: str | None,
1939
+ format: str,
1940
+ recursive: bool,
1941
+ ):
1942
+ """Check for stateless classes that should be module functions.
1943
+
1944
+ Detects Python classes that have no constructor (__init__), no instance
1945
+ state, and 2+ methods - indicating they should be refactored to module-level
1946
+ functions instead of using a class as a namespace.
1947
+
1948
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
1949
+
1950
+ Examples:
1951
+
1952
+ \b
1953
+ # Check current directory (all files recursively)
1954
+ thai-lint stateless-class
1955
+
1956
+ \b
1957
+ # Check specific directory
1958
+ thai-lint stateless-class src/
1959
+
1960
+ \b
1961
+ # Check single file
1962
+ thai-lint stateless-class src/utils.py
1963
+
1964
+ \b
1965
+ # Check multiple files
1966
+ thai-lint stateless-class src/utils.py src/helpers.py
1967
+
1968
+ \b
1969
+ # Get JSON output
1970
+ thai-lint stateless-class --format json .
1971
+
1972
+ \b
1973
+ # Get SARIF output for CI/CD integration
1974
+ thai-lint stateless-class --format sarif src/
1975
+
1976
+ \b
1977
+ # Use custom config file
1978
+ thai-lint stateless-class --config .thailint.yaml src/
1979
+ """
1980
+ verbose = ctx.obj.get("verbose", False)
1981
+ project_root = _get_project_root_from_context(ctx)
1982
+
1983
+ if not paths:
1984
+ paths = (".",)
1985
+
1986
+ path_objs = [Path(p) for p in paths]
1987
+
1988
+ try:
1989
+ _execute_stateless_class_lint(
1990
+ path_objs, config_file, format, recursive, verbose, project_root
1991
+ )
1992
+ except Exception as e:
1993
+ _handle_linting_error(e, verbose)
1994
+
1995
+
1996
+ def _execute_stateless_class_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
1997
+ path_objs, config_file, format, recursive, verbose, project_root=None
1998
+ ):
1999
+ """Execute stateless-class lint."""
2000
+ _validate_paths_exist(path_objs)
2001
+ orchestrator = _setup_stateless_class_orchestrator(
2002
+ path_objs, config_file, verbose, project_root
2003
+ )
2004
+ stateless_class_violations = _run_stateless_class_lint(orchestrator, path_objs, recursive)
2005
+
2006
+ if verbose:
2007
+ logger.info(f"Found {len(stateless_class_violations)} stateless-class violation(s)")
2008
+
2009
+ format_violations(stateless_class_violations, format)
2010
+ sys.exit(1 if stateless_class_violations else 0)
2011
+
2012
+
1898
2013
  if __name__ == "__main__":
1899
2014
  cli()
@@ -151,6 +151,10 @@ class MultiLanguageLintRule(BaseLintRule):
151
151
  - _load_config(context) for configuration loading
152
152
  """
153
153
 
154
+ def __init__(self) -> None:
155
+ """Initialize the multi-language lint rule."""
156
+ pass # Base class for multi-language linters
157
+
154
158
  def check(self, context: BaseLintContext) -> list[Violation]:
155
159
  """Check for violations with automatic language dispatch.
156
160
 
@@ -0,0 +1,158 @@
1
+ """
2
+ Purpose: Automatic rule discovery for plugin-based linter architecture
3
+
4
+ Scope: Discovers and validates linting rules from Python packages
5
+
6
+ Overview: Provides automatic rule discovery functionality for the linter framework. Scans Python
7
+ packages for classes inheriting from BaseLintRule, filters out abstract base classes, validates
8
+ rule classes, and attempts instantiation. Handles import errors gracefully to support partial
9
+ package installations. Enables plugin architecture by discovering rules without explicit registration.
10
+
11
+ Dependencies: importlib, inspect, pkgutil, BaseLintRule
12
+
13
+ Exports: discover_from_package function, RuleDiscovery class (compat)
14
+
15
+ Interfaces: discover_from_package(package_path) -> list[BaseLintRule]
16
+
17
+ Implementation: Package traversal with pkgutil, class introspection with inspect, error handling
18
+ """
19
+
20
+ import importlib
21
+ import inspect
22
+ import pkgutil
23
+ from typing import Any
24
+
25
+ from .base import BaseLintRule
26
+
27
+
28
+ def discover_from_package(package_path: str) -> list[BaseLintRule]:
29
+ """Discover rules from a package and its modules.
30
+
31
+ Args:
32
+ package_path: Python package path (e.g., 'src.linters')
33
+
34
+ Returns:
35
+ List of discovered rule instances
36
+ """
37
+ try:
38
+ package = importlib.import_module(package_path)
39
+ except ImportError:
40
+ return []
41
+
42
+ if not hasattr(package, "__path__"):
43
+ return _discover_from_module(package_path)
44
+
45
+ return _discover_from_package_modules(package_path, package)
46
+
47
+
48
+ def _discover_from_package_modules(package_path: str, package: Any) -> list[BaseLintRule]:
49
+ """Discover rules from all modules in a package.
50
+
51
+ Args:
52
+ package_path: Package path
53
+ package: Imported package object
54
+
55
+ Returns:
56
+ List of discovered rules
57
+ """
58
+ rules = []
59
+ for _, module_name, _ in pkgutil.iter_modules(package.__path__):
60
+ full_module_name = f"{package_path}.{module_name}"
61
+ module_rules = _try_discover_from_module(full_module_name)
62
+ rules.extend(module_rules)
63
+ return rules
64
+
65
+
66
+ def _try_discover_from_module(module_name: str) -> list[BaseLintRule]:
67
+ """Try to discover rules from a module, return empty list on error.
68
+
69
+ Args:
70
+ module_name: Full module name
71
+
72
+ Returns:
73
+ List of discovered rules (empty on error)
74
+ """
75
+ try:
76
+ return _discover_from_module(module_name)
77
+ except (ImportError, AttributeError):
78
+ return []
79
+
80
+
81
+ def _discover_from_module(module_path: str) -> list[BaseLintRule]:
82
+ """Discover rules from a specific module.
83
+
84
+ Args:
85
+ module_path: Full module path to search
86
+
87
+ Returns:
88
+ List of discovered rule instances
89
+ """
90
+ try:
91
+ module = importlib.import_module(module_path)
92
+ except (ImportError, AttributeError):
93
+ return []
94
+
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
103
+
104
+
105
+ def _try_instantiate_rule(rule_class: type[BaseLintRule]) -> BaseLintRule | None:
106
+ """Try to instantiate a rule class.
107
+
108
+ Args:
109
+ rule_class: Rule class to instantiate
110
+
111
+ Returns:
112
+ Rule instance or None on error
113
+ """
114
+ try:
115
+ return rule_class()
116
+ except (TypeError, AttributeError):
117
+ return None
118
+
119
+
120
+ def _is_rule_class(obj: Any) -> bool:
121
+ """Check if an object is a valid rule class.
122
+
123
+ Args:
124
+ obj: Object to check
125
+
126
+ Returns:
127
+ True if obj is a concrete BaseLintRule subclass
128
+ """
129
+ return (
130
+ inspect.isclass(obj)
131
+ and issubclass(obj, BaseLintRule)
132
+ and obj is not BaseLintRule
133
+ and not inspect.isabstract(obj)
134
+ )
135
+
136
+
137
+ # Legacy class wrapper for backward compatibility
138
+ class RuleDiscovery:
139
+ """Discovers linting rules from Python packages.
140
+
141
+ Note: This class is a thin wrapper around module-level functions
142
+ for backward compatibility.
143
+ """
144
+
145
+ def __init__(self) -> None:
146
+ """Initialize the discovery service."""
147
+ pass # No state needed
148
+
149
+ def discover_from_package(self, package_path: str) -> list[BaseLintRule]:
150
+ """Discover rules from a package and its modules.
151
+
152
+ Args:
153
+ package_path: Python package path (e.g., 'src.linters')
154
+
155
+ Returns:
156
+ List of discovered rule instances
157
+ """
158
+ return discover_from_package(package_path)