thailint 0.1.5__py3-none-any.whl → 0.2.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 +498 -141
- src/config.py +6 -31
- src/core/base.py +12 -0
- src/core/cli_utils.py +206 -0
- src/core/config_parser.py +99 -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 +262 -0
- src/linters/dry/block_grouper.py +59 -0
- src/linters/dry/cache.py +218 -0
- src/linters/dry/cache_query.py +61 -0
- src/linters/dry/config.py +130 -0
- src/linters/dry/config_loader.py +44 -0
- src/linters/dry/deduplicator.py +120 -0
- src/linters/dry/duplicate_storage.py +126 -0
- src/linters/dry/file_analyzer.py +127 -0
- src/linters/dry/inline_ignore.py +140 -0
- src/linters/dry/linter.py +170 -0
- src/linters/dry/python_analyzer.py +517 -0
- src/linters/dry/storage_initializer.py +51 -0
- src/linters/dry/token_hasher.py +115 -0
- src/linters/dry/typescript_analyzer.py +590 -0
- src/linters/dry/violation_builder.py +74 -0
- src/linters/dry/violation_filter.py +91 -0
- src/linters/dry/violation_generator.py +174 -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 +252 -472
- 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/nesting/config.py +13 -3
- src/linters/nesting/linter.py +76 -152
- src/linters/nesting/typescript_analyzer.py +38 -102
- src/linters/nesting/typescript_function_extractor.py +130 -0
- src/linters/nesting/violation_builder.py +139 -0
- src/linters/srp/__init__.py +99 -0
- src/linters/srp/class_analyzer.py +113 -0
- src/linters/srp/config.py +76 -0
- src/linters/srp/heuristics.py +89 -0
- src/linters/srp/linter.py +225 -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 +42 -7
- src/utils/__init__.py +4 -0
- src/utils/project_root.py +84 -0
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/METADATA +414 -63
- thailint-0.2.0.dist-info/RECORD +75 -0
- src/.ai/layout.yaml +0 -48
- thailint-0.1.5.dist-info/RECORD +0 -28
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/LICENSE +0 -0
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/WHEEL +0 -0
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
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, BaseLintRule
|
|
22
|
+
from src.core.linter_utils import has_file_content, 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(BaseLintRule):
|
|
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.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
context: Lint context with file information
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List of violations found
|
|
64
|
+
"""
|
|
65
|
+
if not self._should_check_file(context):
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
config = load_linter_config(context, "srp", SRPConfig)
|
|
69
|
+
if not self._is_linter_enabled(context, config):
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
return self._check_by_language(context, config)
|
|
73
|
+
|
|
74
|
+
def _should_check_file(self, context: BaseLintContext) -> bool:
|
|
75
|
+
"""Check if file has content to analyze.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
context: Lint context
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if file should be checked
|
|
82
|
+
"""
|
|
83
|
+
return has_file_content(context)
|
|
84
|
+
|
|
85
|
+
def _is_linter_enabled(self, context: BaseLintContext, config: SRPConfig) -> bool:
|
|
86
|
+
"""Check if linter is enabled and file is not ignored.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
context: Lint context
|
|
90
|
+
config: SRP configuration
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if linter should run on this file
|
|
94
|
+
"""
|
|
95
|
+
return config.enabled and not self._is_file_ignored(context, config)
|
|
96
|
+
|
|
97
|
+
def _check_by_language(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
98
|
+
"""Dispatch to language-specific checker.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
context: Lint context
|
|
102
|
+
config: SRP configuration
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of violations found
|
|
106
|
+
"""
|
|
107
|
+
if context.language == "python":
|
|
108
|
+
return self._check_python(context, config)
|
|
109
|
+
if context.language in ("typescript", "javascript"):
|
|
110
|
+
return self._check_typescript(context, config)
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
def _is_file_ignored(self, context: BaseLintContext, config: SRPConfig) -> bool:
|
|
114
|
+
"""Check if file matches ignore patterns.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
context: Lint context
|
|
118
|
+
config: SRP configuration
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True if file should be ignored
|
|
122
|
+
"""
|
|
123
|
+
if not config.ignore:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
file_path = str(context.file_path)
|
|
127
|
+
for pattern in config.ignore:
|
|
128
|
+
if pattern in file_path:
|
|
129
|
+
return True
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def _check_python(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
133
|
+
"""Check Python code for SRP violations.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
context: Lint context with file information
|
|
137
|
+
config: SRP configuration
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
List of violations found
|
|
141
|
+
"""
|
|
142
|
+
results = self._class_analyzer.analyze_python(context, config)
|
|
143
|
+
if results and isinstance(results[0], Violation): # Syntax errors
|
|
144
|
+
return results # type: ignore[return-value]
|
|
145
|
+
|
|
146
|
+
return self._build_violations_from_metrics(results, config, context)
|
|
147
|
+
|
|
148
|
+
def _build_violations_from_metrics(
|
|
149
|
+
self,
|
|
150
|
+
metrics_list: list,
|
|
151
|
+
config: SRPConfig,
|
|
152
|
+
context: BaseLintContext,
|
|
153
|
+
) -> list[Violation]:
|
|
154
|
+
"""Build violations from class metrics.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
metrics_list: List of class metrics
|
|
158
|
+
config: SRP configuration
|
|
159
|
+
context: Lint context
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of violations
|
|
163
|
+
"""
|
|
164
|
+
violations = []
|
|
165
|
+
for metrics in metrics_list:
|
|
166
|
+
if not isinstance(metrics, dict):
|
|
167
|
+
continue
|
|
168
|
+
violation = self._create_violation_if_needed(metrics, config, context)
|
|
169
|
+
if violation:
|
|
170
|
+
violations.append(violation)
|
|
171
|
+
return violations
|
|
172
|
+
|
|
173
|
+
def _create_violation_if_needed(
|
|
174
|
+
self,
|
|
175
|
+
metrics: dict,
|
|
176
|
+
config: SRPConfig,
|
|
177
|
+
context: BaseLintContext,
|
|
178
|
+
) -> Violation | None:
|
|
179
|
+
"""Create violation if metrics exceed thresholds.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
metrics: Class metrics dictionary
|
|
183
|
+
config: SRP configuration
|
|
184
|
+
context: Lint context
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Violation or None if no issues or should be ignored
|
|
188
|
+
"""
|
|
189
|
+
issues = evaluate_metrics(metrics, config)
|
|
190
|
+
if not issues:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
violation = self._violation_builder.build_violation(metrics, issues, self.rule_id, context)
|
|
194
|
+
if self._should_ignore(violation, context):
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
return violation
|
|
198
|
+
|
|
199
|
+
def _check_typescript(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
200
|
+
"""Check TypeScript code for SRP violations.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
context: Lint context with file information
|
|
204
|
+
config: SRP configuration
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
List of violations found
|
|
208
|
+
"""
|
|
209
|
+
metrics_list = self._class_analyzer.analyze_typescript(context, config)
|
|
210
|
+
return self._build_violations_from_metrics(metrics_list, config, context)
|
|
211
|
+
|
|
212
|
+
def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
213
|
+
"""Check if violation should be ignored based on inline directives.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
violation: Violation to check
|
|
217
|
+
context: Lint context with file content
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
True if violation should be ignored
|
|
221
|
+
"""
|
|
222
|
+
if context.file_content is None:
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
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
|
+
)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Violation creation with suggestions for SRP linter
|
|
3
|
+
|
|
4
|
+
Scope: Builds Violation objects with contextual messages and refactoring suggestions
|
|
5
|
+
|
|
6
|
+
Overview: Provides violation building functionality for the SRP linter. Creates violations
|
|
7
|
+
from class metrics and issue descriptions, generates contextual error messages, and
|
|
8
|
+
provides actionable refactoring suggestions based on issue types (methods, lines, keywords).
|
|
9
|
+
Isolates violation construction and suggestion generation from metrics evaluation and
|
|
10
|
+
class analysis to maintain single responsibility.
|
|
11
|
+
|
|
12
|
+
Dependencies: BaseLintContext, Violation, Severity, typing, src.core.violation_builder
|
|
13
|
+
|
|
14
|
+
Exports: ViolationBuilder
|
|
15
|
+
|
|
16
|
+
Interfaces: build_violation(metrics, issues, rule_id, context) -> Violation
|
|
17
|
+
|
|
18
|
+
Implementation: Formats messages from metrics, generates targeted suggestions per issue type,
|
|
19
|
+
extends BaseViolationBuilder for consistent violation construction
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from src.core.base import BaseLintContext
|
|
25
|
+
from src.core.types import Severity, Violation
|
|
26
|
+
from src.core.violation_builder import BaseViolationBuilder, ViolationInfo
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ViolationBuilder(BaseViolationBuilder):
|
|
30
|
+
"""Builds SRP violations with messages and suggestions."""
|
|
31
|
+
|
|
32
|
+
def build_violation(
|
|
33
|
+
self,
|
|
34
|
+
metrics: dict[str, Any],
|
|
35
|
+
issues: list[str],
|
|
36
|
+
rule_id: str,
|
|
37
|
+
context: BaseLintContext,
|
|
38
|
+
) -> Violation:
|
|
39
|
+
"""Build violation from metrics and issues.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
metrics: Class metrics dictionary
|
|
43
|
+
issues: List of issue descriptions
|
|
44
|
+
rule_id: Rule identifier
|
|
45
|
+
context: Lint context
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Violation with message and suggestion
|
|
49
|
+
"""
|
|
50
|
+
message = f"Class '{metrics['class_name']}' may violate SRP: {', '.join(issues)}"
|
|
51
|
+
suggestion = self._generate_suggestion(issues)
|
|
52
|
+
|
|
53
|
+
info = ViolationInfo(
|
|
54
|
+
rule_id=rule_id,
|
|
55
|
+
file_path=str(context.file_path or ""),
|
|
56
|
+
line=metrics["line"],
|
|
57
|
+
column=metrics["column"],
|
|
58
|
+
message=message,
|
|
59
|
+
severity=Severity.ERROR,
|
|
60
|
+
suggestion=suggestion,
|
|
61
|
+
)
|
|
62
|
+
return self.build(info)
|
|
63
|
+
|
|
64
|
+
def _generate_suggestion(self, issues: list[str]) -> str:
|
|
65
|
+
"""Generate refactoring suggestion based on issues.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
issues: List of issue descriptions
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Suggestion string with refactoring advice
|
|
72
|
+
"""
|
|
73
|
+
suggestions = [
|
|
74
|
+
self._suggest_for_methods(issues),
|
|
75
|
+
self._suggest_for_lines(issues),
|
|
76
|
+
self._suggest_for_keywords(issues),
|
|
77
|
+
]
|
|
78
|
+
return ". ".join(filter(None, suggestions))
|
|
79
|
+
|
|
80
|
+
def _suggest_for_methods(self, issues: list[str]) -> str:
|
|
81
|
+
"""Suggest fix for too many methods.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
issues: List of issue descriptions
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Suggestion string or empty string
|
|
88
|
+
"""
|
|
89
|
+
if any("methods" in issue for issue in issues):
|
|
90
|
+
return "Consider extracting related methods into separate classes"
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
def _suggest_for_lines(self, issues: list[str]) -> str:
|
|
94
|
+
"""Suggest fix for too many lines.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
issues: List of issue descriptions
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Suggestion string or empty string
|
|
101
|
+
"""
|
|
102
|
+
if any("lines" in issue for issue in issues):
|
|
103
|
+
return "Consider breaking the class into smaller, focused classes"
|
|
104
|
+
return ""
|
|
105
|
+
|
|
106
|
+
def _suggest_for_keywords(self, issues: list[str]) -> str:
|
|
107
|
+
"""Suggest fix for responsibility keywords.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
issues: List of issue descriptions
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Suggestion string or empty string
|
|
114
|
+
"""
|
|
115
|
+
if any("keyword" in issue for issue in issues):
|
|
116
|
+
return "Avoid generic names like Manager, Handler, Processor"
|
|
117
|
+
return ""
|