thailint 0.7.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.
- {thailint-0.7.0 → thailint-0.9.0}/CHANGELOG.md +13 -0
- {thailint-0.7.0 → thailint-0.9.0}/PKG-INFO +119 -3
- {thailint-0.7.0 → thailint-0.9.0}/README.md +117 -1
- {thailint-0.7.0 → thailint-0.9.0}/pyproject.toml +2 -2
- {thailint-0.7.0 → thailint-0.9.0}/src/cli.py +233 -1
- {thailint-0.7.0 → thailint-0.9.0}/src/core/base.py +4 -0
- thailint-0.9.0/src/core/rule_discovery.py +158 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/core/violation_builder.py +75 -15
- {thailint-0.7.0 → thailint-0.9.0}/src/linter_config/loader.py +45 -12
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/block_filter.py +15 -8
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/block_grouper.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/cache.py +3 -2
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/cache_query.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/duplicate_storage.py +5 -4
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/token_hasher.py +5 -1
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/violation_builder.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/violation_filter.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/violation_generator.py +1 -1
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_header/bash_parser.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_placement/directory_matcher.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_placement/pattern_matcher.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_placement/pattern_validator.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/magic_numbers/context_analyzer.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/magic_numbers/typescript_analyzer.py +4 -0
- thailint-0.9.0/src/linters/method_property/__init__.py +49 -0
- thailint-0.9.0/src/linters/method_property/config.py +135 -0
- thailint-0.9.0/src/linters/method_property/linter.py +419 -0
- thailint-0.9.0/src/linters/method_property/python_analyzer.py +472 -0
- thailint-0.9.0/src/linters/method_property/violation_builder.py +116 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/nesting/python_analyzer.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/nesting/typescript_function_extractor.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/print_statements/typescript_analyzer.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/srp/class_analyzer.py +4 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/srp/python_analyzer.py +55 -20
- thailint-0.9.0/src/linters/srp/typescript_metrics_calculator.py +126 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/srp/violation_builder.py +4 -0
- thailint-0.9.0/src/linters/stateless_class/__init__.py +25 -0
- thailint-0.9.0/src/linters/stateless_class/config.py +58 -0
- thailint-0.9.0/src/linters/stateless_class/linter.py +355 -0
- thailint-0.9.0/src/linters/stateless_class/python_analyzer.py +299 -0
- thailint-0.7.0/src/core/rule_discovery.py +0 -132
- thailint-0.7.0/src/linters/srp/typescript_metrics_calculator.py +0 -90
- {thailint-0.7.0 → thailint-0.9.0}/LICENSE +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/analyzers/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/analyzers/typescript_base.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/api.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/config.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/core/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/core/cli_utils.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/core/config_parser.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/core/linter_utils.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/core/registry.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/core/types.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/formatters/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/formatters/sarif.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linter_config/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linter_config/ignore.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/base_token_analyzer.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/config.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/config_loader.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/deduplicator.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/file_analyzer.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/inline_ignore.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/linter.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/python_analyzer.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/storage_initializer.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/dry/typescript_analyzer.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_header/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_header/atemporal_detector.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_header/base_parser.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_header/config.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_header/css_parser.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_header/field_validator.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_header/linter.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_header/markdown_parser.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_header/python_parser.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_header/typescript_parser.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_header/violation_builder.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_placement/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_placement/config_loader.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_placement/linter.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_placement/path_resolver.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_placement/rule_checker.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/file_placement/violation_factory.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/magic_numbers/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/magic_numbers/config.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/magic_numbers/linter.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/magic_numbers/python_analyzer.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/magic_numbers/violation_builder.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/nesting/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/nesting/config.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/nesting/linter.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/nesting/typescript_analyzer.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/nesting/violation_builder.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/print_statements/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/print_statements/config.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/print_statements/linter.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/print_statements/python_analyzer.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/print_statements/violation_builder.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/srp/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/srp/config.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/srp/heuristics.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/srp/linter.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/srp/metrics_evaluator.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/linters/srp/typescript_analyzer.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/orchestrator/__init__.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/orchestrator/core.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/orchestrator/language_detector.py +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/templates/thailint_config_template.yaml +0 -0
- {thailint-0.7.0 → thailint-0.9.0}/src/utils/__init__.py +0 -0
- {thailint-0.7.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,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thailint
|
|
3
|
-
Version: 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
|
|
7
7
|
Keywords: linter,ai,code-quality,static-analysis,file-placement,governance,multi-language,cli,docker,python
|
|
8
8
|
Author: Steve Jackson
|
|
9
9
|
Requires-Python: >=3.11,<4.0
|
|
10
|
-
Classifier: Development Status ::
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Environment :: Console
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
|
|
38
38
|
[](https://opensource.org/licenses/MIT)
|
|
39
39
|
[](https://www.python.org/downloads/)
|
|
40
|
-
[](tests/)
|
|
41
41
|
[](htmlcov/)
|
|
42
42
|
[](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
|
|
43
43
|
[](docs/sarif-output.md)
|
|
@@ -98,6 +98,18 @@ 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
|
+
- **Method Property Linting** - Detect methods that should be @property decorators
|
|
102
|
+
- Python AST-based detection
|
|
103
|
+
- get_* prefix detection (Java-style getters)
|
|
104
|
+
- Simple computed value detection
|
|
105
|
+
- Action verb exclusion (to_*, finalize, serialize)
|
|
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
|
|
101
113
|
- **Pluggable Architecture** - Easy to extend with custom linters
|
|
102
114
|
- **Multi-Language Support** - Python, TypeScript, JavaScript, and more
|
|
103
115
|
- **Flexible Configuration** - YAML/JSON configs with pattern matching
|
|
@@ -1011,6 +1023,108 @@ Overview: Created 2024-01-15. # thailint: ignore[file-header]
|
|
|
1011
1023
|
|
|
1012
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.
|
|
1013
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
|
+
|
|
1014
1128
|
## Pre-commit Hooks
|
|
1015
1129
|
|
|
1016
1130
|
Automate code quality checks before every commit and push with pre-commit hooks.
|
|
@@ -1300,6 +1414,8 @@ docker run --rm -v /path/to/workspace:/workspace \
|
|
|
1300
1414
|
- **[Nesting Depth Linter](https://thai-lint.readthedocs.io/en/latest/nesting-linter/)** - Nesting depth analysis guide
|
|
1301
1415
|
- **[SRP Linter](https://thai-lint.readthedocs.io/en/latest/srp-linter/)** - Single Responsibility Principle guide
|
|
1302
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
|
|
1303
1419
|
- **[Pre-commit Hooks](https://thai-lint.readthedocs.io/en/latest/pre-commit-hooks/)** - Automated quality checks
|
|
1304
1420
|
- **[SARIF Output Guide](docs/sarif-output.md)** - SARIF format for GitHub Code Scanning and CI/CD
|
|
1305
1421
|
- **[Publishing Guide](https://thai-lint.readthedocs.io/en/latest/releasing/)** - Release and publishing workflow
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
4
|
[](https://www.python.org/downloads/)
|
|
5
|
-
[](tests/)
|
|
6
6
|
[](htmlcov/)
|
|
7
7
|
[](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
|
|
8
8
|
[](docs/sarif-output.md)
|
|
@@ -63,6 +63,18 @@ 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
|
+
- **Method Property Linting** - Detect methods that should be @property decorators
|
|
67
|
+
- Python AST-based detection
|
|
68
|
+
- get_* prefix detection (Java-style getters)
|
|
69
|
+
- Simple computed value detection
|
|
70
|
+
- Action verb exclusion (to_*, finalize, serialize)
|
|
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
|
|
66
78
|
- **Pluggable Architecture** - Easy to extend with custom linters
|
|
67
79
|
- **Multi-Language Support** - Python, TypeScript, JavaScript, and more
|
|
68
80
|
- **Flexible Configuration** - YAML/JSON configs with pattern matching
|
|
@@ -976,6 +988,108 @@ Overview: Created 2024-01-15. # thailint: ignore[file-header]
|
|
|
976
988
|
|
|
977
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.
|
|
978
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
|
+
|
|
979
1093
|
## Pre-commit Hooks
|
|
980
1094
|
|
|
981
1095
|
Automate code quality checks before every commit and push with pre-commit hooks.
|
|
@@ -1265,6 +1379,8 @@ docker run --rm -v /path/to/workspace:/workspace \
|
|
|
1265
1379
|
- **[Nesting Depth Linter](https://thai-lint.readthedocs.io/en/latest/nesting-linter/)** - Nesting depth analysis guide
|
|
1266
1380
|
- **[SRP Linter](https://thai-lint.readthedocs.io/en/latest/srp-linter/)** - Single Responsibility Principle guide
|
|
1267
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
|
|
1268
1384
|
- **[Pre-commit Hooks](https://thai-lint.readthedocs.io/en/latest/pre-commit-hooks/)** - Automated quality checks
|
|
1269
1385
|
- **[SARIF Output Guide](docs/sarif-output.md)** - SARIF format for GitHub Code Scanning and CI/CD
|
|
1270
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.
|
|
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"
|
|
@@ -38,7 +38,7 @@ keywords = [
|
|
|
38
38
|
"python",
|
|
39
39
|
]
|
|
40
40
|
classifiers = [
|
|
41
|
-
"Development Status ::
|
|
41
|
+
"Development Status :: 4 - Beta",
|
|
42
42
|
"Intended Audience :: Developers",
|
|
43
43
|
"License :: OSI Approved :: MIT License",
|
|
44
44
|
"Programming Language :: Python :: 3",
|
|
@@ -11,7 +11,7 @@ Overview: Provides the main CLI application using Click decorators for command d
|
|
|
11
11
|
|
|
12
12
|
Dependencies: click for CLI framework, logging for structured output, pathlib for file paths
|
|
13
13
|
|
|
14
|
-
Exports: cli (main command group), hello command, config command group,
|
|
14
|
+
Exports: cli (main command group), hello command, config command group, linter commands
|
|
15
15
|
|
|
16
16
|
Interfaces: Click CLI commands, configuration context via Click ctx, logging integration
|
|
17
17
|
|
|
@@ -1778,5 +1778,237 @@ def _execute_file_header_lint( # pylint: disable=too-many-arguments,too-many-po
|
|
|
1778
1778
|
sys.exit(1 if file_header_violations else 0)
|
|
1779
1779
|
|
|
1780
1780
|
|
|
1781
|
+
# =============================================================================
|
|
1782
|
+
# Method Property Linter Command
|
|
1783
|
+
# =============================================================================
|
|
1784
|
+
|
|
1785
|
+
|
|
1786
|
+
def _setup_method_property_orchestrator(
|
|
1787
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
1788
|
+
):
|
|
1789
|
+
"""Set up orchestrator for method-property command."""
|
|
1790
|
+
from src.orchestrator.core import Orchestrator
|
|
1791
|
+
from src.utils.project_root import get_project_root
|
|
1792
|
+
|
|
1793
|
+
if project_root is None:
|
|
1794
|
+
first_path = path_objs[0] if path_objs else Path.cwd()
|
|
1795
|
+
search_start = first_path if first_path.is_dir() else first_path.parent
|
|
1796
|
+
project_root = get_project_root(search_start)
|
|
1797
|
+
|
|
1798
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
1799
|
+
|
|
1800
|
+
if config_file:
|
|
1801
|
+
_load_config_file(orchestrator, config_file, verbose)
|
|
1802
|
+
|
|
1803
|
+
return orchestrator
|
|
1804
|
+
|
|
1805
|
+
|
|
1806
|
+
def _run_method_property_lint(orchestrator, path_objs: list[Path], recursive: bool):
|
|
1807
|
+
"""Execute method-property lint on files or directories."""
|
|
1808
|
+
all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
1809
|
+
return [v for v in all_violations if "method-property" in v.rule_id]
|
|
1810
|
+
|
|
1811
|
+
|
|
1812
|
+
@cli.command("method-property")
|
|
1813
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
1814
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
1815
|
+
@format_option
|
|
1816
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
1817
|
+
@click.pass_context
|
|
1818
|
+
def method_property(
|
|
1819
|
+
ctx,
|
|
1820
|
+
paths: tuple[str, ...],
|
|
1821
|
+
config_file: str | None,
|
|
1822
|
+
format: str,
|
|
1823
|
+
recursive: bool,
|
|
1824
|
+
):
|
|
1825
|
+
"""Check for methods that should be @property decorators.
|
|
1826
|
+
|
|
1827
|
+
Detects Python methods that could be converted to properties following
|
|
1828
|
+
Pythonic conventions:
|
|
1829
|
+
- Methods returning only self._attribute or self.attribute
|
|
1830
|
+
- get_* prefixed methods (Java-style getters)
|
|
1831
|
+
- Simple computed values with no side effects
|
|
1832
|
+
|
|
1833
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
1834
|
+
|
|
1835
|
+
Examples:
|
|
1836
|
+
|
|
1837
|
+
\b
|
|
1838
|
+
# Check current directory (all files recursively)
|
|
1839
|
+
thai-lint method-property
|
|
1840
|
+
|
|
1841
|
+
\b
|
|
1842
|
+
# Check specific directory
|
|
1843
|
+
thai-lint method-property src/
|
|
1844
|
+
|
|
1845
|
+
\b
|
|
1846
|
+
# Check single file
|
|
1847
|
+
thai-lint method-property src/models.py
|
|
1848
|
+
|
|
1849
|
+
\b
|
|
1850
|
+
# Check multiple files
|
|
1851
|
+
thai-lint method-property src/models.py src/services.py
|
|
1852
|
+
|
|
1853
|
+
\b
|
|
1854
|
+
# Get JSON output
|
|
1855
|
+
thai-lint method-property --format json .
|
|
1856
|
+
|
|
1857
|
+
\b
|
|
1858
|
+
# Get SARIF output for CI/CD integration
|
|
1859
|
+
thai-lint method-property --format sarif src/
|
|
1860
|
+
|
|
1861
|
+
\b
|
|
1862
|
+
# Use custom config file
|
|
1863
|
+
thai-lint method-property --config .thailint.yaml src/
|
|
1864
|
+
"""
|
|
1865
|
+
verbose = ctx.obj.get("verbose", False)
|
|
1866
|
+
project_root = _get_project_root_from_context(ctx)
|
|
1867
|
+
|
|
1868
|
+
if not paths:
|
|
1869
|
+
paths = (".",)
|
|
1870
|
+
|
|
1871
|
+
path_objs = [Path(p) for p in paths]
|
|
1872
|
+
|
|
1873
|
+
try:
|
|
1874
|
+
_execute_method_property_lint(
|
|
1875
|
+
path_objs, config_file, format, recursive, verbose, project_root
|
|
1876
|
+
)
|
|
1877
|
+
except Exception as e:
|
|
1878
|
+
_handle_linting_error(e, verbose)
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
def _execute_method_property_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
1882
|
+
path_objs, config_file, format, recursive, verbose, project_root=None
|
|
1883
|
+
):
|
|
1884
|
+
"""Execute method-property lint."""
|
|
1885
|
+
_validate_paths_exist(path_objs)
|
|
1886
|
+
orchestrator = _setup_method_property_orchestrator(
|
|
1887
|
+
path_objs, config_file, verbose, project_root
|
|
1888
|
+
)
|
|
1889
|
+
method_property_violations = _run_method_property_lint(orchestrator, path_objs, recursive)
|
|
1890
|
+
|
|
1891
|
+
if verbose:
|
|
1892
|
+
logger.info(f"Found {len(method_property_violations)} method-property violation(s)")
|
|
1893
|
+
|
|
1894
|
+
format_violations(method_property_violations, format)
|
|
1895
|
+
sys.exit(1 if method_property_violations else 0)
|
|
1896
|
+
|
|
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
|
+
|
|
1781
2013
|
if __name__ == "__main__":
|
|
1782
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
|
|