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.
Files changed (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {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 IgnoreDirectiveParser
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 = IgnoreDirectiveParser()
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 == "python":
107
+ if context.language == Language.PYTHON:
104
108
  return self._check_python(context, config)
105
109
 
106
- if context.language in ("typescript", "javascript"):
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
- 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
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: PythonSRPAnalyzer class for analyzing Python classes using AST
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
- classes = []
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
- 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
- }
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), 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.
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
- 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
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
- 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
- )
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
+ )