thailint 0.1.5__py3-none-any.whl → 0.5.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/__init__.py +7 -2
- src/analyzers/__init__.py +23 -0
- src/analyzers/typescript_base.py +148 -0
- src/api.py +1 -1
- src/cli.py +1111 -144
- src/config.py +12 -33
- src/core/base.py +102 -5
- src/core/cli_utils.py +206 -0
- src/core/config_parser.py +126 -0
- src/core/linter_utils.py +168 -0
- src/core/registry.py +17 -92
- src/core/rule_discovery.py +132 -0
- src/core/violation_builder.py +122 -0
- src/linter_config/ignore.py +112 -40
- src/linter_config/loader.py +3 -13
- src/linters/dry/__init__.py +23 -0
- src/linters/dry/base_token_analyzer.py +76 -0
- src/linters/dry/block_filter.py +265 -0
- src/linters/dry/block_grouper.py +59 -0
- src/linters/dry/cache.py +172 -0
- src/linters/dry/cache_query.py +61 -0
- src/linters/dry/config.py +134 -0
- src/linters/dry/config_loader.py +44 -0
- src/linters/dry/deduplicator.py +120 -0
- src/linters/dry/duplicate_storage.py +63 -0
- src/linters/dry/file_analyzer.py +90 -0
- src/linters/dry/inline_ignore.py +140 -0
- src/linters/dry/linter.py +163 -0
- src/linters/dry/python_analyzer.py +668 -0
- src/linters/dry/storage_initializer.py +42 -0
- src/linters/dry/token_hasher.py +169 -0
- src/linters/dry/typescript_analyzer.py +592 -0
- src/linters/dry/violation_builder.py +74 -0
- src/linters/dry/violation_filter.py +94 -0
- src/linters/dry/violation_generator.py +174 -0
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +87 -0
- src/linters/file_header/config.py +66 -0
- src/linters/file_header/field_validator.py +69 -0
- src/linters/file_header/linter.py +313 -0
- src/linters/file_header/python_parser.py +86 -0
- src/linters/file_header/violation_builder.py +78 -0
- src/linters/file_placement/config_loader.py +86 -0
- src/linters/file_placement/directory_matcher.py +80 -0
- src/linters/file_placement/linter.py +262 -471
- src/linters/file_placement/path_resolver.py +61 -0
- src/linters/file_placement/pattern_matcher.py +55 -0
- src/linters/file_placement/pattern_validator.py +106 -0
- src/linters/file_placement/rule_checker.py +229 -0
- src/linters/file_placement/violation_factory.py +177 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +247 -0
- src/linters/magic_numbers/linter.py +516 -0
- src/linters/magic_numbers/python_analyzer.py +76 -0
- src/linters/magic_numbers/typescript_analyzer.py +218 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +17 -4
- src/linters/nesting/linter.py +81 -168
- src/linters/nesting/typescript_analyzer.py +39 -102
- src/linters/nesting/typescript_function_extractor.py +130 -0
- src/linters/nesting/violation_builder.py +139 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +83 -0
- src/linters/print_statements/linter.py +430 -0
- src/linters/print_statements/python_analyzer.py +155 -0
- src/linters/print_statements/typescript_analyzer.py +135 -0
- src/linters/print_statements/violation_builder.py +98 -0
- src/linters/srp/__init__.py +99 -0
- src/linters/srp/class_analyzer.py +113 -0
- src/linters/srp/config.py +82 -0
- src/linters/srp/heuristics.py +89 -0
- src/linters/srp/linter.py +234 -0
- src/linters/srp/metrics_evaluator.py +47 -0
- src/linters/srp/python_analyzer.py +72 -0
- src/linters/srp/typescript_analyzer.py +75 -0
- src/linters/srp/typescript_metrics_calculator.py +90 -0
- src/linters/srp/violation_builder.py +117 -0
- src/orchestrator/core.py +54 -9
- src/templates/thailint_config_template.yaml +158 -0
- src/utils/__init__.py +4 -0
- src/utils/project_root.py +203 -0
- thailint-0.5.0.dist-info/METADATA +1286 -0
- thailint-0.5.0.dist-info/RECORD +96 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
- src/.ai/layout.yaml +0 -48
- thailint-0.1.5.dist-info/METADATA +0 -629
- thailint-0.1.5.dist-info/RECORD +0 -28
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: SRP detection heuristics for analyzing code complexity and responsibility
|
|
3
|
+
|
|
4
|
+
Scope: Helper functions for method counting, LOC calculation, and keyword detection
|
|
5
|
+
|
|
6
|
+
Overview: Provides heuristic-based analysis functions for detecting Single Responsibility
|
|
7
|
+
Principle violations. Implements method counting that excludes property decorators and
|
|
8
|
+
special methods. Provides LOC calculation that filters out blank lines and comments.
|
|
9
|
+
Includes keyword detection for identifying generic class names that often indicate SRP
|
|
10
|
+
violations (Manager, Handler, etc.). Supports both Python AST and TypeScript tree-sitter
|
|
11
|
+
nodes. These heuristics enable practical SRP detection without requiring perfect semantic
|
|
12
|
+
analysis, focusing on measurable code metrics that correlate with responsibility scope.
|
|
13
|
+
|
|
14
|
+
Dependencies: ast module for Python AST analysis, typing for type hints
|
|
15
|
+
|
|
16
|
+
Exports: count_methods, count_loc, has_responsibility_keyword, has_property_decorator
|
|
17
|
+
|
|
18
|
+
Interfaces: Functions accepting AST nodes and returning metrics (int, bool)
|
|
19
|
+
|
|
20
|
+
Implementation: AST walking with filtering logic, heuristic-based thresholds
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def count_methods(class_node: ast.ClassDef) -> int:
|
|
27
|
+
"""Count methods in a class (excludes properties and special methods).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
class_node: AST node representing a class definition
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Number of methods in the class
|
|
34
|
+
"""
|
|
35
|
+
methods = 0
|
|
36
|
+
for node in class_node.body:
|
|
37
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
38
|
+
continue
|
|
39
|
+
# Don't count @property decorators as methods
|
|
40
|
+
if not has_property_decorator(node):
|
|
41
|
+
methods += 1
|
|
42
|
+
return methods
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def count_loc(class_node: ast.ClassDef, source: str) -> int:
|
|
46
|
+
"""Count lines of code in a class (excludes blank lines and comments).
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
class_node: AST node representing a class definition
|
|
50
|
+
source: Full source code of the file
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Number of code lines in the class
|
|
54
|
+
"""
|
|
55
|
+
start_line = class_node.lineno
|
|
56
|
+
end_line = class_node.end_lineno or start_line
|
|
57
|
+
lines = source.split("\n")[start_line - 1 : end_line]
|
|
58
|
+
|
|
59
|
+
# Filter out blank lines and comments
|
|
60
|
+
code_lines = [line for line in lines if line.strip() and not line.strip().startswith("#")]
|
|
61
|
+
return len(code_lines)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def has_responsibility_keyword(class_name: str, keywords: list[str]) -> bool:
|
|
65
|
+
"""Check if class name contains responsibility keywords.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
class_name: Name of the class to check
|
|
69
|
+
keywords: List of keywords indicating potential SRP violations
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if class name contains any responsibility keyword
|
|
73
|
+
"""
|
|
74
|
+
return any(keyword in class_name for keyword in keywords)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def has_property_decorator(func_node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
78
|
+
"""Check if function has @property decorator.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
func_node: AST node representing a function definition
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if function has @property decorator
|
|
85
|
+
"""
|
|
86
|
+
for decorator in func_node.decorator_list:
|
|
87
|
+
if isinstance(decorator, ast.Name) and decorator.id == "property":
|
|
88
|
+
return True
|
|
89
|
+
return False
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Main SRP linter rule implementation
|
|
3
|
+
|
|
4
|
+
Scope: SRPRule class implementing BaseLintRule interface
|
|
5
|
+
|
|
6
|
+
Overview: Implements Single Responsibility Principle linter rule following BaseLintRule interface.
|
|
7
|
+
Orchestrates configuration loading, class analysis, metrics evaluation, and violation building
|
|
8
|
+
through focused helper classes. Detects classes with too many methods, excessive lines of code,
|
|
9
|
+
or generic naming patterns. Supports configurable thresholds and ignore directives. Handles both
|
|
10
|
+
Python and TypeScript code analysis. Main rule class acts as coordinator for SRP checking workflow.
|
|
11
|
+
|
|
12
|
+
Dependencies: BaseLintRule, BaseLintContext, Violation, ClassAnalyzer, MetricsEvaluator, ViolationBuilder
|
|
13
|
+
|
|
14
|
+
Exports: SRPRule class
|
|
15
|
+
|
|
16
|
+
Interfaces: SRPRule.check(context) -> list[Violation], properties for rule metadata
|
|
17
|
+
|
|
18
|
+
Implementation: Composition pattern with helper classes, heuristic-based SRP analysis
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
22
|
+
from src.core.linter_utils import load_linter_config
|
|
23
|
+
from src.core.types import Violation
|
|
24
|
+
from src.linter_config.ignore import IgnoreDirectiveParser
|
|
25
|
+
|
|
26
|
+
from .class_analyzer import ClassAnalyzer
|
|
27
|
+
from .config import SRPConfig
|
|
28
|
+
from .metrics_evaluator import evaluate_metrics
|
|
29
|
+
from .violation_builder import ViolationBuilder
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SRPRule(MultiLanguageLintRule):
|
|
33
|
+
"""Detects Single Responsibility Principle violations in classes."""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
"""Initialize the SRP rule."""
|
|
37
|
+
self._ignore_parser = IgnoreDirectiveParser()
|
|
38
|
+
self._class_analyzer = ClassAnalyzer()
|
|
39
|
+
self._violation_builder = ViolationBuilder()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def rule_id(self) -> str:
|
|
43
|
+
"""Unique identifier for this rule."""
|
|
44
|
+
return "srp.violation"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def rule_name(self) -> str:
|
|
48
|
+
"""Human-readable name for this rule."""
|
|
49
|
+
return "Single Responsibility Principle"
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def description(self) -> str:
|
|
53
|
+
"""Description of what this rule checks."""
|
|
54
|
+
return "Classes should have a single, well-defined responsibility"
|
|
55
|
+
|
|
56
|
+
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
57
|
+
"""Check for SRP violations with custom ignore pattern handling.
|
|
58
|
+
|
|
59
|
+
Overrides parent to add file-level ignore pattern checking before dispatch.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
context: Lint context with file information
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List of violations found
|
|
66
|
+
"""
|
|
67
|
+
from src.core.linter_utils import has_file_content
|
|
68
|
+
|
|
69
|
+
if not has_file_content(context):
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
config = self._load_config(context)
|
|
73
|
+
if not self._should_process_file(context, config):
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
# Standard language dispatch
|
|
77
|
+
return self._dispatch_by_language(context, config)
|
|
78
|
+
|
|
79
|
+
def _should_process_file(self, context: BaseLintContext, config: SRPConfig) -> bool:
|
|
80
|
+
"""Check if file should be processed.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
context: Lint context
|
|
84
|
+
config: SRP configuration
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if file should be processed
|
|
88
|
+
"""
|
|
89
|
+
if not config.enabled:
|
|
90
|
+
return False
|
|
91
|
+
return not self._is_file_ignored(context, config)
|
|
92
|
+
|
|
93
|
+
def _dispatch_by_language(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
94
|
+
"""Dispatch to language-specific checker.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
context: Lint context
|
|
98
|
+
config: SRP configuration
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of violations found
|
|
102
|
+
"""
|
|
103
|
+
if context.language == "python":
|
|
104
|
+
return self._check_python(context, config)
|
|
105
|
+
|
|
106
|
+
if context.language in ("typescript", "javascript"):
|
|
107
|
+
return self._check_typescript(context, config)
|
|
108
|
+
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
def _load_config(self, context: BaseLintContext) -> SRPConfig:
|
|
112
|
+
"""Load configuration from context.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
context: Lint context
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
SRPConfig instance
|
|
119
|
+
"""
|
|
120
|
+
return load_linter_config(context, "srp", SRPConfig)
|
|
121
|
+
|
|
122
|
+
def _is_file_ignored(self, context: BaseLintContext, config: SRPConfig) -> bool:
|
|
123
|
+
"""Check if file matches ignore patterns.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
context: Lint context
|
|
127
|
+
config: SRP configuration
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
True if file should be ignored
|
|
131
|
+
"""
|
|
132
|
+
if not config.ignore:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
file_path = str(context.file_path)
|
|
136
|
+
for pattern in config.ignore:
|
|
137
|
+
if pattern in file_path:
|
|
138
|
+
return True
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def _check_python(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
142
|
+
"""Check Python code for SRP violations.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
context: Lint context with file information
|
|
146
|
+
config: SRP configuration
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
List of violations found
|
|
150
|
+
"""
|
|
151
|
+
results = self._class_analyzer.analyze_python(context, config)
|
|
152
|
+
if results and isinstance(results[0], Violation): # Syntax errors
|
|
153
|
+
return results # type: ignore[return-value]
|
|
154
|
+
|
|
155
|
+
return self._build_violations_from_metrics(results, config, context)
|
|
156
|
+
|
|
157
|
+
def _build_violations_from_metrics(
|
|
158
|
+
self,
|
|
159
|
+
metrics_list: list,
|
|
160
|
+
config: SRPConfig,
|
|
161
|
+
context: BaseLintContext,
|
|
162
|
+
) -> list[Violation]:
|
|
163
|
+
"""Build violations from class metrics.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
metrics_list: List of class metrics
|
|
167
|
+
config: SRP configuration
|
|
168
|
+
context: Lint context
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
List of violations
|
|
172
|
+
"""
|
|
173
|
+
violations = []
|
|
174
|
+
for metrics in metrics_list:
|
|
175
|
+
if not isinstance(metrics, dict):
|
|
176
|
+
continue
|
|
177
|
+
violation = self._create_violation_if_needed(metrics, config, context)
|
|
178
|
+
if violation:
|
|
179
|
+
violations.append(violation)
|
|
180
|
+
return violations
|
|
181
|
+
|
|
182
|
+
def _create_violation_if_needed(
|
|
183
|
+
self,
|
|
184
|
+
metrics: dict,
|
|
185
|
+
config: SRPConfig,
|
|
186
|
+
context: BaseLintContext,
|
|
187
|
+
) -> Violation | None:
|
|
188
|
+
"""Create violation if metrics exceed thresholds.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
metrics: Class metrics dictionary
|
|
192
|
+
config: SRP configuration
|
|
193
|
+
context: Lint context
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Violation or None if no issues or should be ignored
|
|
197
|
+
"""
|
|
198
|
+
issues = evaluate_metrics(metrics, config)
|
|
199
|
+
if not issues:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
violation = self._violation_builder.build_violation(metrics, issues, self.rule_id, context)
|
|
203
|
+
if self._should_ignore(violation, context):
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
return violation
|
|
207
|
+
|
|
208
|
+
def _check_typescript(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
209
|
+
"""Check TypeScript code for SRP violations.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
context: Lint context with file information
|
|
213
|
+
config: SRP configuration
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
List of violations found
|
|
217
|
+
"""
|
|
218
|
+
metrics_list = self._class_analyzer.analyze_typescript(context, config)
|
|
219
|
+
return self._build_violations_from_metrics(metrics_list, config, context)
|
|
220
|
+
|
|
221
|
+
def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
222
|
+
"""Check if violation should be ignored based on inline directives.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
violation: Violation to check
|
|
226
|
+
context: Lint context with file content
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
True if violation should be ignored
|
|
230
|
+
"""
|
|
231
|
+
if context.file_content is None:
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
return self._ignore_parser.should_ignore_violation(violation, context.file_content)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: SRP metrics evaluation against configured thresholds
|
|
3
|
+
|
|
4
|
+
Scope: Evaluates class metrics to determine SRP violations
|
|
5
|
+
|
|
6
|
+
Overview: Provides metrics evaluation functionality for the SRP linter. Checks class metrics
|
|
7
|
+
(method count, lines of code, naming keywords) against configured thresholds. Collects
|
|
8
|
+
issues when metrics exceed limits. Isolates threshold evaluation logic from class analysis
|
|
9
|
+
and violation building to maintain single responsibility.
|
|
10
|
+
|
|
11
|
+
Dependencies: SRPConfig, typing
|
|
12
|
+
|
|
13
|
+
Exports: evaluate_metrics
|
|
14
|
+
|
|
15
|
+
Interfaces: evaluate_metrics(metrics, config) -> list[str] (returns list of issue descriptions)
|
|
16
|
+
|
|
17
|
+
Implementation: Compares numeric thresholds and keyword patterns, returns descriptive issue strings
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from .config import SRPConfig
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def evaluate_metrics(metrics: dict[str, Any], config: SRPConfig) -> list[str]:
|
|
26
|
+
"""Evaluate class metrics and collect SRP issues.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
metrics: Class metrics dictionary with method_count, loc, has_keyword
|
|
30
|
+
config: SRP configuration with thresholds
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of issue descriptions (empty if no violations)
|
|
34
|
+
"""
|
|
35
|
+
issues = []
|
|
36
|
+
|
|
37
|
+
# Check numeric thresholds
|
|
38
|
+
if metrics["method_count"] > config.max_methods:
|
|
39
|
+
issues.append(f"{metrics['method_count']} methods (max: {config.max_methods})")
|
|
40
|
+
if metrics["loc"] > config.max_loc:
|
|
41
|
+
issues.append(f"{metrics['loc']} lines (max: {config.max_loc})")
|
|
42
|
+
|
|
43
|
+
# Check keyword heuristic
|
|
44
|
+
if config.check_keywords and metrics["has_keyword"]:
|
|
45
|
+
issues.append("responsibility keyword in name")
|
|
46
|
+
|
|
47
|
+
return issues
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Python AST analyzer for detecting SRP violations in Python classes
|
|
3
|
+
|
|
4
|
+
Scope: PythonSRPAnalyzer class for analyzing Python classes using AST
|
|
5
|
+
|
|
6
|
+
Overview: Implements Python-specific SRP analysis using the ast module to parse and analyze
|
|
7
|
+
class definitions. Walks the AST to find all class definitions, then analyzes each class
|
|
8
|
+
for SRP violation indicators: method count, lines of code, and responsibility keywords.
|
|
9
|
+
Collects comprehensive metrics including class name, method count, LOC, keyword presence,
|
|
10
|
+
and location information (line, column). Integrates with heuristics module for metric
|
|
11
|
+
calculation. Returns structured metric dictionaries that the main linter uses to create
|
|
12
|
+
violations. Handles nested classes by analyzing all classes in the tree.
|
|
13
|
+
|
|
14
|
+
Dependencies: ast module for Python AST parsing, typing for type hints, heuristics module
|
|
15
|
+
|
|
16
|
+
Exports: PythonSRPAnalyzer class
|
|
17
|
+
|
|
18
|
+
Interfaces: find_all_classes(tree), analyze_class(class_node, source, config)
|
|
19
|
+
|
|
20
|
+
Implementation: AST walking pattern, metric collection, integration with heuristics module
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from .config import SRPConfig
|
|
27
|
+
from .heuristics import count_loc, count_methods, has_responsibility_keyword
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PythonSRPAnalyzer:
|
|
31
|
+
"""Analyzes Python classes for SRP violations."""
|
|
32
|
+
|
|
33
|
+
def find_all_classes(self, tree: ast.AST) -> list[ast.ClassDef]:
|
|
34
|
+
"""Find all class definitions in AST.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
tree: Root AST node to search
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List of all class definition nodes
|
|
41
|
+
"""
|
|
42
|
+
classes = []
|
|
43
|
+
for node in ast.walk(tree):
|
|
44
|
+
if isinstance(node, ast.ClassDef):
|
|
45
|
+
classes.append(node)
|
|
46
|
+
return classes
|
|
47
|
+
|
|
48
|
+
def analyze_class(
|
|
49
|
+
self, class_node: ast.ClassDef, source: str, config: SRPConfig
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
"""Analyze a class for SRP metrics.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
class_node: AST node representing a class definition
|
|
55
|
+
source: Full source code of the file
|
|
56
|
+
config: SRP configuration with thresholds and keywords
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Dictionary with class metrics (name, method_count, loc, etc.)
|
|
60
|
+
"""
|
|
61
|
+
method_count = count_methods(class_node)
|
|
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
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: TypeScript AST analyzer for detecting SRP violations in TypeScript classes
|
|
3
|
+
|
|
4
|
+
Scope: TypeScriptSRPAnalyzer class for analyzing TypeScript classes using tree-sitter
|
|
5
|
+
|
|
6
|
+
Overview: Implements TypeScript-specific SRP analysis using tree-sitter parser. Extends
|
|
7
|
+
TypeScriptBaseAnalyzer to reuse common tree-sitter initialization and traversal patterns.
|
|
8
|
+
Walks the AST to find all class declarations, analyzes each class for SRP violation
|
|
9
|
+
indicators using metrics calculator helper. Collects comprehensive metrics including
|
|
10
|
+
class name, method count, LOC, keyword presence, and location information. Delegates
|
|
11
|
+
metrics calculation to TypeScriptMetricsCalculator.
|
|
12
|
+
|
|
13
|
+
Dependencies: TypeScriptBaseAnalyzer, TypeScriptMetricsCalculator, SRPConfig
|
|
14
|
+
|
|
15
|
+
Exports: TypeScriptSRPAnalyzer class
|
|
16
|
+
|
|
17
|
+
Interfaces: find_all_classes(tree), analyze_class(class_node, source, config)
|
|
18
|
+
|
|
19
|
+
Implementation: Inherits tree-sitter parsing from base, composition with metrics calculator
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
|
|
25
|
+
|
|
26
|
+
from .config import SRPConfig
|
|
27
|
+
from .typescript_metrics_calculator import TypeScriptMetricsCalculator
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TypeScriptSRPAnalyzer(TypeScriptBaseAnalyzer):
|
|
31
|
+
"""Analyzes TypeScript classes for SRP violations."""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
"""Initialize analyzer with metrics calculator."""
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.metrics_calculator = TypeScriptMetricsCalculator()
|
|
37
|
+
|
|
38
|
+
def find_all_classes(self, root_node: Any) -> list[Any]:
|
|
39
|
+
"""Find all class declarations in TypeScript AST.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
root_node: Root tree-sitter node to search
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
List of all class declaration nodes
|
|
46
|
+
"""
|
|
47
|
+
return self.walk_tree(root_node, "class_declaration")
|
|
48
|
+
|
|
49
|
+
def analyze_class(self, class_node: Any, source: str, config: SRPConfig) -> dict[str, Any]:
|
|
50
|
+
"""Analyze a TypeScript class for SRP metrics.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
class_node: Tree-sitter node representing a class declaration
|
|
54
|
+
source: Full source code of the file
|
|
55
|
+
config: SRP configuration with thresholds and keywords
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dictionary with class metrics (name, method_count, loc, etc.)
|
|
59
|
+
"""
|
|
60
|
+
class_name = self.extract_identifier_name(class_node)
|
|
61
|
+
if class_name == "anonymous":
|
|
62
|
+
class_name = "UnnamedClass"
|
|
63
|
+
|
|
64
|
+
method_count = self.metrics_calculator.count_methods(class_node)
|
|
65
|
+
loc = self.metrics_calculator.count_loc(class_node, source)
|
|
66
|
+
has_keyword = any(keyword in class_name for keyword in config.keywords)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"class_name": class_name,
|
|
70
|
+
"method_count": method_count,
|
|
71
|
+
"loc": loc,
|
|
72
|
+
"has_keyword": has_keyword,
|
|
73
|
+
"line": class_node.start_point[0] + 1,
|
|
74
|
+
"column": class_node.start_point[1],
|
|
75
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: TypeScript class metrics calculation for SRP analysis
|
|
3
|
+
|
|
4
|
+
Scope: Calculates method count and lines of code for TypeScript classes
|
|
5
|
+
|
|
6
|
+
Overview: Provides metrics calculation functionality for TypeScript classes in SRP analysis. Counts
|
|
7
|
+
public methods in class bodies (excludes constructors), calculates lines of code from AST node
|
|
8
|
+
positions, and identifies class body nodes. Uses tree-sitter AST node types. Isolates metrics
|
|
9
|
+
calculation from class analysis and tree traversal logic.
|
|
10
|
+
|
|
11
|
+
Dependencies: typing
|
|
12
|
+
|
|
13
|
+
Exports: TypeScriptMetricsCalculator
|
|
14
|
+
|
|
15
|
+
Interfaces: count_methods(class_node), count_loc(class_node, source)
|
|
16
|
+
|
|
17
|
+
Implementation: Tree-sitter node type matching, AST position arithmetic
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TypeScriptMetricsCalculator:
|
|
24
|
+
"""Calculates metrics for TypeScript classes."""
|
|
25
|
+
|
|
26
|
+
def count_methods(self, class_node: Any) -> int:
|
|
27
|
+
"""Count number of methods in a TypeScript class.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
class_node: Class declaration tree-sitter node
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Number of public methods (excludes constructor)
|
|
34
|
+
"""
|
|
35
|
+
class_body = self._get_class_body(class_node)
|
|
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
|
|
45
|
+
|
|
46
|
+
def count_loc(self, class_node: Any, source: str) -> int:
|
|
47
|
+
"""Count lines of code in a TypeScript class.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
class_node: Class declaration tree-sitter node
|
|
51
|
+
source: Full source code string
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Number of lines in class definition
|
|
55
|
+
"""
|
|
56
|
+
start_line = class_node.start_point[0]
|
|
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
|
+
)
|