thailint 0.7.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/cli.py +233 -1
- src/core/base.py +4 -0
- src/core/rule_discovery.py +110 -84
- src/core/violation_builder.py +75 -15
- src/linter_config/loader.py +45 -12
- src/linters/dry/block_filter.py +15 -8
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +3 -2
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/token_hasher.py +5 -1
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +4 -0
- src/linters/dry/violation_generator.py +1 -1
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/pattern_matcher.py +4 -0
- src/linters/file_placement/pattern_validator.py +4 -0
- src/linters/magic_numbers/context_analyzer.py +4 -0
- src/linters/magic_numbers/typescript_analyzer.py +4 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +135 -0
- src/linters/method_property/linter.py +419 -0
- src/linters/method_property/python_analyzer.py +472 -0
- src/linters/method_property/violation_builder.py +116 -0
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_function_extractor.py +4 -0
- src/linters/print_statements/typescript_analyzer.py +4 -0
- src/linters/srp/class_analyzer.py +4 -0
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +83 -47
- src/linters/srp/violation_builder.py +4 -0
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +355 -0
- src/linters/stateless_class/python_analyzer.py +299 -0
- {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/METADATA +119 -3
- {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/RECORD +41 -32
- {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/WHEEL +0 -0
- {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Purpose: Python AST analyzer for detecting SRP violations in Python classes
|
|
3
3
|
|
|
4
|
-
Scope:
|
|
4
|
+
Scope: Functions for analyzing Python classes using AST
|
|
5
5
|
|
|
6
6
|
Overview: Implements Python-specific SRP analysis using the ast module to parse and analyze
|
|
7
7
|
class definitions. Walks the AST to find all class definitions, then analyzes each class
|
|
@@ -13,7 +13,7 @@ Overview: Implements Python-specific SRP analysis using the ast module to parse
|
|
|
13
13
|
|
|
14
14
|
Dependencies: ast module for Python AST parsing, typing for type hints, heuristics module
|
|
15
15
|
|
|
16
|
-
Exports: PythonSRPAnalyzer class
|
|
16
|
+
Exports: find_all_classes function, analyze_class function, PythonSRPAnalyzer class (compat)
|
|
17
17
|
|
|
18
18
|
Interfaces: find_all_classes(tree), analyze_class(class_node, source, config)
|
|
19
19
|
|
|
@@ -27,8 +27,58 @@ from .config import SRPConfig
|
|
|
27
27
|
from .heuristics import count_loc, count_methods, has_responsibility_keyword
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def find_all_classes(tree: ast.AST) -> list[ast.ClassDef]:
|
|
31
|
+
"""Find all class definitions in AST.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
tree: Root AST node to search
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of all class definition nodes
|
|
38
|
+
"""
|
|
39
|
+
classes = []
|
|
40
|
+
for node in ast.walk(tree):
|
|
41
|
+
if isinstance(node, ast.ClassDef):
|
|
42
|
+
classes.append(node)
|
|
43
|
+
return classes
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def analyze_class(class_node: ast.ClassDef, source: str, config: SRPConfig) -> dict[str, Any]:
|
|
47
|
+
"""Analyze a class for SRP metrics.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
class_node: AST node representing a class definition
|
|
51
|
+
source: Full source code of the file
|
|
52
|
+
config: SRP configuration with thresholds and keywords
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dictionary with class metrics (name, method_count, loc, etc.)
|
|
56
|
+
"""
|
|
57
|
+
method_count = count_methods(class_node)
|
|
58
|
+
loc = count_loc(class_node, source)
|
|
59
|
+
has_keyword = has_responsibility_keyword(class_node.name, config.keywords)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
"class_name": class_node.name,
|
|
63
|
+
"method_count": method_count,
|
|
64
|
+
"loc": loc,
|
|
65
|
+
"has_keyword": has_keyword,
|
|
66
|
+
"line": class_node.lineno,
|
|
67
|
+
"column": class_node.col_offset,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Legacy class wrapper for backward compatibility
|
|
30
72
|
class PythonSRPAnalyzer:
|
|
31
|
-
"""Analyzes Python classes for SRP violations.
|
|
73
|
+
"""Analyzes Python classes for SRP violations.
|
|
74
|
+
|
|
75
|
+
Note: This class is a thin wrapper around module-level functions
|
|
76
|
+
for backward compatibility.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
"""Initialize the analyzer."""
|
|
81
|
+
pass # No state needed
|
|
32
82
|
|
|
33
83
|
def find_all_classes(self, tree: ast.AST) -> list[ast.ClassDef]:
|
|
34
84
|
"""Find all class definitions in AST.
|
|
@@ -39,11 +89,7 @@ class PythonSRPAnalyzer:
|
|
|
39
89
|
Returns:
|
|
40
90
|
List of all class definition nodes
|
|
41
91
|
"""
|
|
42
|
-
|
|
43
|
-
for node in ast.walk(tree):
|
|
44
|
-
if isinstance(node, ast.ClassDef):
|
|
45
|
-
classes.append(node)
|
|
46
|
-
return classes
|
|
92
|
+
return find_all_classes(tree)
|
|
47
93
|
|
|
48
94
|
def analyze_class(
|
|
49
95
|
self, class_node: ast.ClassDef, source: str, config: SRPConfig
|
|
@@ -58,15 +104,4 @@ class PythonSRPAnalyzer:
|
|
|
58
104
|
Returns:
|
|
59
105
|
Dictionary with class metrics (name, method_count, loc, etc.)
|
|
60
106
|
"""
|
|
61
|
-
|
|
62
|
-
loc = count_loc(class_node, source)
|
|
63
|
-
has_keyword = has_responsibility_keyword(class_node.name, config.keywords)
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
"class_name": class_node.name,
|
|
67
|
-
"method_count": method_count,
|
|
68
|
-
"loc": loc,
|
|
69
|
-
"has_keyword": has_keyword,
|
|
70
|
-
"line": class_node.lineno,
|
|
71
|
-
"column": class_node.col_offset,
|
|
72
|
-
}
|
|
107
|
+
return analyze_class(class_node, source, config)
|
|
@@ -10,7 +10,7 @@ Overview: Provides metrics calculation functionality for TypeScript classes in S
|
|
|
10
10
|
|
|
11
11
|
Dependencies: typing
|
|
12
12
|
|
|
13
|
-
Exports: TypeScriptMetricsCalculator
|
|
13
|
+
Exports: count_methods function, count_loc function, TypeScriptMetricsCalculator class (compat)
|
|
14
14
|
|
|
15
15
|
Interfaces: count_methods(class_node), count_loc(class_node, source)
|
|
16
16
|
|
|
@@ -20,8 +20,87 @@ Implementation: Tree-sitter node type matching, AST position arithmetic
|
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def count_methods(class_node: Any) -> int:
|
|
24
|
+
"""Count number of methods in a TypeScript class.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
class_node: Class declaration tree-sitter node
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Number of public methods (excludes constructor)
|
|
31
|
+
"""
|
|
32
|
+
class_body = _get_class_body(class_node)
|
|
33
|
+
if not class_body:
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
method_count = 0
|
|
37
|
+
for child in class_body.children:
|
|
38
|
+
if _is_countable_method(child):
|
|
39
|
+
method_count += 1
|
|
40
|
+
|
|
41
|
+
return method_count
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def count_loc(class_node: Any, source: str) -> int:
|
|
45
|
+
"""Count lines of code in a TypeScript class.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
class_node: Class declaration tree-sitter node
|
|
49
|
+
source: Full source code string
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Number of lines in class definition
|
|
53
|
+
"""
|
|
54
|
+
start_line = class_node.start_point[0]
|
|
55
|
+
end_line = class_node.end_point[0]
|
|
56
|
+
return end_line - start_line + 1
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_class_body(class_node: Any) -> Any:
|
|
60
|
+
"""Get the class_body node from a class declaration.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
class_node: Class declaration node
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Class body node or None
|
|
67
|
+
"""
|
|
68
|
+
for child in class_node.children:
|
|
69
|
+
if child.type == "class_body":
|
|
70
|
+
return child
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_countable_method(node: Any) -> bool:
|
|
75
|
+
"""Check if node is a method that should be counted.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
node: Tree-sitter node to check
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if node is a countable method
|
|
82
|
+
"""
|
|
83
|
+
if node.type != "method_definition":
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
# Check if it's a constructor
|
|
87
|
+
return all(
|
|
88
|
+
not (child.type == "property_identifier" and child.text.decode() == "constructor")
|
|
89
|
+
for child in node.children
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Legacy class wrapper for backward compatibility
|
|
23
94
|
class TypeScriptMetricsCalculator:
|
|
24
|
-
"""Calculates metrics for TypeScript classes.
|
|
95
|
+
"""Calculates metrics for TypeScript classes.
|
|
96
|
+
|
|
97
|
+
Note: This class is a thin wrapper around module-level functions
|
|
98
|
+
for backward compatibility.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self) -> None:
|
|
102
|
+
"""Initialize the metrics calculator."""
|
|
103
|
+
pass # No state needed
|
|
25
104
|
|
|
26
105
|
def count_methods(self, class_node: Any) -> int:
|
|
27
106
|
"""Count number of methods in a TypeScript class.
|
|
@@ -32,16 +111,7 @@ class TypeScriptMetricsCalculator:
|
|
|
32
111
|
Returns:
|
|
33
112
|
Number of public methods (excludes constructor)
|
|
34
113
|
"""
|
|
35
|
-
|
|
36
|
-
if not class_body:
|
|
37
|
-
return 0
|
|
38
|
-
|
|
39
|
-
method_count = 0
|
|
40
|
-
for child in class_body.children:
|
|
41
|
-
if self._is_countable_method(child):
|
|
42
|
-
method_count += 1
|
|
43
|
-
|
|
44
|
-
return method_count
|
|
114
|
+
return count_methods(class_node)
|
|
45
115
|
|
|
46
116
|
def count_loc(self, class_node: Any, source: str) -> int:
|
|
47
117
|
"""Count lines of code in a TypeScript class.
|
|
@@ -53,38 +123,4 @@ class TypeScriptMetricsCalculator:
|
|
|
53
123
|
Returns:
|
|
54
124
|
Number of lines in class definition
|
|
55
125
|
"""
|
|
56
|
-
|
|
57
|
-
end_line = class_node.end_point[0]
|
|
58
|
-
return end_line - start_line + 1
|
|
59
|
-
|
|
60
|
-
def _get_class_body(self, class_node: Any) -> Any:
|
|
61
|
-
"""Get the class_body node from a class declaration.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
class_node: Class declaration node
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
Class body node or None
|
|
68
|
-
"""
|
|
69
|
-
for child in class_node.children:
|
|
70
|
-
if child.type == "class_body":
|
|
71
|
-
return child
|
|
72
|
-
return None
|
|
73
|
-
|
|
74
|
-
def _is_countable_method(self, node: Any) -> bool:
|
|
75
|
-
"""Check if node is a method that should be counted.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
node: Tree-sitter node to check
|
|
79
|
-
|
|
80
|
-
Returns:
|
|
81
|
-
True if node is a countable method
|
|
82
|
-
"""
|
|
83
|
-
if node.type != "method_definition":
|
|
84
|
-
return False
|
|
85
|
-
|
|
86
|
-
# Check if it's a constructor
|
|
87
|
-
return all(
|
|
88
|
-
not (child.type == "property_identifier" and child.text.decode() == "constructor")
|
|
89
|
-
for child in node.children
|
|
90
|
-
)
|
|
126
|
+
return count_loc(class_node, source)
|
|
@@ -29,6 +29,10 @@ from src.core.violation_builder import BaseViolationBuilder, ViolationInfo
|
|
|
29
29
|
class ViolationBuilder(BaseViolationBuilder):
|
|
30
30
|
"""Builds SRP violations with messages and suggestions."""
|
|
31
31
|
|
|
32
|
+
def __init__(self) -> None: # pylint: disable=useless-parent-delegation
|
|
33
|
+
"""Initialize the violation builder."""
|
|
34
|
+
super().__init__() # Inherits from BaseViolationBuilder
|
|
35
|
+
|
|
32
36
|
def build_violation(
|
|
33
37
|
self,
|
|
34
38
|
metrics: dict[str, Any],
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Stateless class linter package for detecting classes without state
|
|
3
|
+
|
|
4
|
+
Scope: Python classes that should be refactored to module-level functions
|
|
5
|
+
|
|
6
|
+
Overview: Package for detecting Python classes that have no constructor (__init__
|
|
7
|
+
or __new__) and no instance state (self.attr assignments), indicating they should
|
|
8
|
+
be refactored to module-level functions. Identifies a common anti-pattern in
|
|
9
|
+
AI-generated code where classes are used as namespaces rather than for object-
|
|
10
|
+
oriented encapsulation.
|
|
11
|
+
|
|
12
|
+
Dependencies: Python AST module, base linter framework
|
|
13
|
+
|
|
14
|
+
Exports: StatelessClassRule - main rule for detecting stateless classes
|
|
15
|
+
|
|
16
|
+
Interfaces: StatelessClassRule.check(context) -> list[Violation]
|
|
17
|
+
|
|
18
|
+
Implementation: AST-based analysis checking for constructor methods and instance
|
|
19
|
+
attribute assignments while excluding legitimate patterns (ABC, Protocol, decorators)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .linter import StatelessClassRule
|
|
23
|
+
from .python_analyzer import ClassInfo, StatelessClassAnalyzer
|
|
24
|
+
|
|
25
|
+
__all__ = ["StatelessClassRule", "StatelessClassAnalyzer", "ClassInfo"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration schema for stateless-class linter
|
|
3
|
+
|
|
4
|
+
Scope: Stateless class linter configuration for Python files
|
|
5
|
+
|
|
6
|
+
Overview: Defines configuration schema for stateless-class linter. Provides
|
|
7
|
+
StatelessClassConfig dataclass with enabled flag, min_methods threshold (default 2)
|
|
8
|
+
for determining minimum methods required to flag a class as stateless, and ignore
|
|
9
|
+
patterns list for excluding specific files or directories. Supports per-file and
|
|
10
|
+
per-directory config overrides through from_dict class method. Integrates with
|
|
11
|
+
orchestrator's configuration system via .thailint.yaml.
|
|
12
|
+
|
|
13
|
+
Dependencies: dataclasses module for configuration structure, typing module for type hints
|
|
14
|
+
|
|
15
|
+
Exports: StatelessClassConfig dataclass
|
|
16
|
+
|
|
17
|
+
Interfaces: from_dict(config, language) -> StatelessClassConfig for configuration loading
|
|
18
|
+
|
|
19
|
+
Implementation: Dataclass with defaults matching stateless class detection conventions
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class StatelessClassConfig:
|
|
28
|
+
"""Configuration for stateless-class linter."""
|
|
29
|
+
|
|
30
|
+
enabled: bool = True
|
|
31
|
+
min_methods: int = 2
|
|
32
|
+
ignore: list[str] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_dict(
|
|
36
|
+
cls, config: dict[str, Any] | None, language: str | None = None
|
|
37
|
+
) -> "StatelessClassConfig":
|
|
38
|
+
"""Load configuration from dictionary.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
config: Dictionary containing configuration values, or None
|
|
42
|
+
language: Programming language (unused, for interface compatibility)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
StatelessClassConfig instance with values from dictionary
|
|
46
|
+
"""
|
|
47
|
+
if config is None:
|
|
48
|
+
return cls()
|
|
49
|
+
|
|
50
|
+
ignore_patterns = config.get("ignore", [])
|
|
51
|
+
if not isinstance(ignore_patterns, list):
|
|
52
|
+
ignore_patterns = []
|
|
53
|
+
|
|
54
|
+
return cls(
|
|
55
|
+
enabled=config.get("enabled", True),
|
|
56
|
+
min_methods=config.get("min_methods", 2),
|
|
57
|
+
ignore=ignore_patterns,
|
|
58
|
+
)
|