thailint 0.5.0__py3-none-any.whl → 0.15.3__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 +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
src/linters/srp/linter.py
CHANGED
|
@@ -16,12 +16,16 @@ Exports: SRPRule class
|
|
|
16
16
|
Interfaces: SRPRule.check(context) -> list[Violation], properties for rule metadata
|
|
17
17
|
|
|
18
18
|
Implementation: Composition pattern with helper classes, heuristic-based SRP analysis
|
|
19
|
+
|
|
20
|
+
Suppressions:
|
|
21
|
+
- type:ignore[return-value]: Generic TypeScript analyzer return type variance
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
24
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
25
|
+
from src.core.constants import Language
|
|
22
26
|
from src.core.linter_utils import load_linter_config
|
|
23
27
|
from src.core.types import Violation
|
|
24
|
-
from src.linter_config.ignore import
|
|
28
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
25
29
|
|
|
26
30
|
from .class_analyzer import ClassAnalyzer
|
|
27
31
|
from .config import SRPConfig
|
|
@@ -34,7 +38,7 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
34
38
|
|
|
35
39
|
def __init__(self) -> None:
|
|
36
40
|
"""Initialize the SRP rule."""
|
|
37
|
-
self._ignore_parser =
|
|
41
|
+
self._ignore_parser = get_ignore_parser()
|
|
38
42
|
self._class_analyzer = ClassAnalyzer()
|
|
39
43
|
self._violation_builder = ViolationBuilder()
|
|
40
44
|
|
|
@@ -100,10 +104,10 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
100
104
|
Returns:
|
|
101
105
|
List of violations found
|
|
102
106
|
"""
|
|
103
|
-
if context.language ==
|
|
107
|
+
if context.language == Language.PYTHON:
|
|
104
108
|
return self._check_python(context, config)
|
|
105
109
|
|
|
106
|
-
if context.language in (
|
|
110
|
+
if context.language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
|
|
107
111
|
return self._check_typescript(context, config)
|
|
108
112
|
|
|
109
113
|
return []
|
|
@@ -133,10 +137,7 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
133
137
|
return False
|
|
134
138
|
|
|
135
139
|
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
|
+
return any(pattern in file_path for pattern in config.ignore)
|
|
140
141
|
|
|
141
142
|
def _check_python(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
142
143
|
"""Check Python code for SRP violations.
|
|
@@ -170,14 +171,12 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
170
171
|
Returns:
|
|
171
172
|
List of violations
|
|
172
173
|
"""
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
violation
|
|
178
|
-
|
|
179
|
-
violations.append(violation)
|
|
180
|
-
return violations
|
|
174
|
+
valid_metrics = (m for m in metrics_list if isinstance(m, dict))
|
|
175
|
+
return [
|
|
176
|
+
violation
|
|
177
|
+
for metrics in valid_metrics
|
|
178
|
+
if (violation := self._create_violation_if_needed(metrics, config, context))
|
|
179
|
+
]
|
|
181
180
|
|
|
182
181
|
def _create_violation_if_needed(
|
|
183
182
|
self,
|
|
@@ -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)
|
|
@@ -4,13 +4,14 @@ Purpose: TypeScript class metrics calculation for SRP analysis
|
|
|
4
4
|
Scope: Calculates method count and lines of code for TypeScript classes
|
|
5
5
|
|
|
6
6
|
Overview: Provides metrics calculation functionality for TypeScript classes in SRP analysis. Counts
|
|
7
|
-
public methods in class bodies (excludes constructors
|
|
8
|
-
positions, and identifies class body nodes. Uses
|
|
9
|
-
calculation from class analysis and tree
|
|
7
|
+
public methods in class bodies (excludes constructors and private methods starting with _),
|
|
8
|
+
calculates lines of code from AST node positions, and identifies class body nodes. Uses
|
|
9
|
+
tree-sitter AST node types. Isolates metrics calculation from class analysis and tree
|
|
10
|
+
traversal logic.
|
|
10
11
|
|
|
11
12
|
Dependencies: typing
|
|
12
13
|
|
|
13
|
-
Exports: TypeScriptMetricsCalculator
|
|
14
|
+
Exports: count_methods function, count_loc function, TypeScriptMetricsCalculator class (compat)
|
|
14
15
|
|
|
15
16
|
Interfaces: count_methods(class_node), count_loc(class_node, source)
|
|
16
17
|
|
|
@@ -20,8 +21,110 @@ Implementation: Tree-sitter node type matching, AST position arithmetic
|
|
|
20
21
|
from typing import Any
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def count_methods(class_node: Any) -> int:
|
|
25
|
+
"""Count number of methods in a TypeScript class.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
class_node: Class declaration tree-sitter node
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Number of public methods (excludes constructor)
|
|
32
|
+
"""
|
|
33
|
+
class_body = _get_class_body(class_node)
|
|
34
|
+
if not class_body:
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
method_count = 0
|
|
38
|
+
for child in class_body.children:
|
|
39
|
+
if _is_countable_method(child):
|
|
40
|
+
method_count += 1
|
|
41
|
+
|
|
42
|
+
return method_count
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def count_loc(class_node: Any, source: str) -> int:
|
|
46
|
+
"""Count lines of code in a TypeScript class.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
class_node: Class declaration tree-sitter node
|
|
50
|
+
source: Full source code string
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Number of lines in class definition
|
|
54
|
+
"""
|
|
55
|
+
start_line = class_node.start_point[0]
|
|
56
|
+
end_line = class_node.end_point[0]
|
|
57
|
+
return end_line - start_line + 1
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_class_body(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
|
+
|
|
75
|
+
def _is_countable_method(node: Any) -> bool:
|
|
76
|
+
"""Check if node is a public method that should be counted.
|
|
77
|
+
|
|
78
|
+
Excludes constructors and private methods (names starting with _).
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
node: Tree-sitter node to check
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if node is a countable public method
|
|
85
|
+
"""
|
|
86
|
+
if node.type != "method_definition":
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
method_name = _get_method_name(node)
|
|
90
|
+
|
|
91
|
+
# Don't count constructors
|
|
92
|
+
if method_name == "constructor":
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
# Don't count private methods (underscore prefix convention)
|
|
96
|
+
if method_name and method_name.startswith("_"):
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _get_method_name(node: Any) -> str | None:
|
|
103
|
+
"""Extract method name from method_definition node.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
node: Method definition tree-sitter node
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Method name or None if not found
|
|
110
|
+
"""
|
|
111
|
+
for child in node.children:
|
|
112
|
+
if child.type == "property_identifier":
|
|
113
|
+
return child.text.decode()
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Legacy class wrapper for backward compatibility
|
|
23
118
|
class TypeScriptMetricsCalculator:
|
|
24
|
-
"""Calculates metrics for TypeScript classes.
|
|
119
|
+
"""Calculates metrics for TypeScript classes.
|
|
120
|
+
|
|
121
|
+
Note: This class is a thin wrapper around module-level functions
|
|
122
|
+
for backward compatibility.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self) -> None:
|
|
126
|
+
"""Initialize the metrics calculator."""
|
|
127
|
+
pass # No state needed
|
|
25
128
|
|
|
26
129
|
def count_methods(self, class_node: Any) -> int:
|
|
27
130
|
"""Count number of methods in a TypeScript class.
|
|
@@ -32,16 +135,7 @@ class TypeScriptMetricsCalculator:
|
|
|
32
135
|
Returns:
|
|
33
136
|
Number of public methods (excludes constructor)
|
|
34
137
|
"""
|
|
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
|
|
138
|
+
return count_methods(class_node)
|
|
45
139
|
|
|
46
140
|
def count_loc(self, class_node: Any, source: str) -> int:
|
|
47
141
|
"""Count lines of code in a TypeScript class.
|
|
@@ -53,38 +147,4 @@ class TypeScriptMetricsCalculator:
|
|
|
53
147
|
Returns:
|
|
54
148
|
Number of lines in class definition
|
|
55
149
|
"""
|
|
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
|
-
)
|
|
150
|
+
return count_loc(class_node, source)
|
|
@@ -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
|
+
)
|