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.
Files changed (91) hide show
  1. src/__init__.py +7 -2
  2. src/analyzers/__init__.py +23 -0
  3. src/analyzers/typescript_base.py +148 -0
  4. src/api.py +1 -1
  5. src/cli.py +1111 -144
  6. src/config.py +12 -33
  7. src/core/base.py +102 -5
  8. src/core/cli_utils.py +206 -0
  9. src/core/config_parser.py +126 -0
  10. src/core/linter_utils.py +168 -0
  11. src/core/registry.py +17 -92
  12. src/core/rule_discovery.py +132 -0
  13. src/core/violation_builder.py +122 -0
  14. src/linter_config/ignore.py +112 -40
  15. src/linter_config/loader.py +3 -13
  16. src/linters/dry/__init__.py +23 -0
  17. src/linters/dry/base_token_analyzer.py +76 -0
  18. src/linters/dry/block_filter.py +265 -0
  19. src/linters/dry/block_grouper.py +59 -0
  20. src/linters/dry/cache.py +172 -0
  21. src/linters/dry/cache_query.py +61 -0
  22. src/linters/dry/config.py +134 -0
  23. src/linters/dry/config_loader.py +44 -0
  24. src/linters/dry/deduplicator.py +120 -0
  25. src/linters/dry/duplicate_storage.py +63 -0
  26. src/linters/dry/file_analyzer.py +90 -0
  27. src/linters/dry/inline_ignore.py +140 -0
  28. src/linters/dry/linter.py +163 -0
  29. src/linters/dry/python_analyzer.py +668 -0
  30. src/linters/dry/storage_initializer.py +42 -0
  31. src/linters/dry/token_hasher.py +169 -0
  32. src/linters/dry/typescript_analyzer.py +592 -0
  33. src/linters/dry/violation_builder.py +74 -0
  34. src/linters/dry/violation_filter.py +94 -0
  35. src/linters/dry/violation_generator.py +174 -0
  36. src/linters/file_header/__init__.py +24 -0
  37. src/linters/file_header/atemporal_detector.py +87 -0
  38. src/linters/file_header/config.py +66 -0
  39. src/linters/file_header/field_validator.py +69 -0
  40. src/linters/file_header/linter.py +313 -0
  41. src/linters/file_header/python_parser.py +86 -0
  42. src/linters/file_header/violation_builder.py +78 -0
  43. src/linters/file_placement/config_loader.py +86 -0
  44. src/linters/file_placement/directory_matcher.py +80 -0
  45. src/linters/file_placement/linter.py +262 -471
  46. src/linters/file_placement/path_resolver.py +61 -0
  47. src/linters/file_placement/pattern_matcher.py +55 -0
  48. src/linters/file_placement/pattern_validator.py +106 -0
  49. src/linters/file_placement/rule_checker.py +229 -0
  50. src/linters/file_placement/violation_factory.py +177 -0
  51. src/linters/magic_numbers/__init__.py +48 -0
  52. src/linters/magic_numbers/config.py +82 -0
  53. src/linters/magic_numbers/context_analyzer.py +247 -0
  54. src/linters/magic_numbers/linter.py +516 -0
  55. src/linters/magic_numbers/python_analyzer.py +76 -0
  56. src/linters/magic_numbers/typescript_analyzer.py +218 -0
  57. src/linters/magic_numbers/violation_builder.py +98 -0
  58. src/linters/nesting/__init__.py +6 -2
  59. src/linters/nesting/config.py +17 -4
  60. src/linters/nesting/linter.py +81 -168
  61. src/linters/nesting/typescript_analyzer.py +39 -102
  62. src/linters/nesting/typescript_function_extractor.py +130 -0
  63. src/linters/nesting/violation_builder.py +139 -0
  64. src/linters/print_statements/__init__.py +53 -0
  65. src/linters/print_statements/config.py +83 -0
  66. src/linters/print_statements/linter.py +430 -0
  67. src/linters/print_statements/python_analyzer.py +155 -0
  68. src/linters/print_statements/typescript_analyzer.py +135 -0
  69. src/linters/print_statements/violation_builder.py +98 -0
  70. src/linters/srp/__init__.py +99 -0
  71. src/linters/srp/class_analyzer.py +113 -0
  72. src/linters/srp/config.py +82 -0
  73. src/linters/srp/heuristics.py +89 -0
  74. src/linters/srp/linter.py +234 -0
  75. src/linters/srp/metrics_evaluator.py +47 -0
  76. src/linters/srp/python_analyzer.py +72 -0
  77. src/linters/srp/typescript_analyzer.py +75 -0
  78. src/linters/srp/typescript_metrics_calculator.py +90 -0
  79. src/linters/srp/violation_builder.py +117 -0
  80. src/orchestrator/core.py +54 -9
  81. src/templates/thailint_config_template.yaml +158 -0
  82. src/utils/__init__.py +4 -0
  83. src/utils/project_root.py +203 -0
  84. thailint-0.5.0.dist-info/METADATA +1286 -0
  85. thailint-0.5.0.dist-info/RECORD +96 -0
  86. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
  87. src/.ai/layout.yaml +0 -48
  88. thailint-0.1.5.dist-info/METADATA +0 -629
  89. thailint-0.1.5.dist-info/RECORD +0 -28
  90. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
  91. {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
+ )