thailint 0.2.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 (214) 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 +44 -27
  23. src/core/base.py +95 -5
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +36 -6
  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 +125 -22
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +142 -94
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +68 -21
  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 +20 -82
  73. src/linters/dry/file_analyzer.py +15 -50
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +182 -54
  76. src/linters/dry/python_analyzer.py +108 -336
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/storage_initializer.py +9 -18
  80. src/linters/dry/token_hasher.py +129 -71
  81. src/linters/dry/typescript_analyzer.py +68 -380
  82. src/linters/dry/typescript_constant_extractor.py +138 -0
  83. src/linters/dry/typescript_statement_detector.py +255 -0
  84. src/linters/dry/typescript_value_extractor.py +70 -0
  85. src/linters/dry/violation_builder.py +4 -0
  86. src/linters/dry/violation_filter.py +9 -5
  87. src/linters/dry/violation_generator.py +71 -14
  88. src/linters/file_header/__init__.py +24 -0
  89. src/linters/file_header/atemporal_detector.py +105 -0
  90. src/linters/file_header/base_parser.py +93 -0
  91. src/linters/file_header/bash_parser.py +66 -0
  92. src/linters/file_header/config.py +140 -0
  93. src/linters/file_header/css_parser.py +70 -0
  94. src/linters/file_header/field_validator.py +72 -0
  95. src/linters/file_header/linter.py +309 -0
  96. src/linters/file_header/markdown_parser.py +130 -0
  97. src/linters/file_header/python_parser.py +42 -0
  98. src/linters/file_header/typescript_parser.py +73 -0
  99. src/linters/file_header/violation_builder.py +79 -0
  100. src/linters/file_placement/config_loader.py +3 -1
  101. src/linters/file_placement/directory_matcher.py +4 -0
  102. src/linters/file_placement/linter.py +74 -31
  103. src/linters/file_placement/pattern_matcher.py +41 -6
  104. src/linters/file_placement/pattern_validator.py +31 -12
  105. src/linters/file_placement/rule_checker.py +12 -7
  106. src/linters/lazy_ignores/__init__.py +43 -0
  107. src/linters/lazy_ignores/config.py +74 -0
  108. src/linters/lazy_ignores/directive_utils.py +164 -0
  109. src/linters/lazy_ignores/header_parser.py +177 -0
  110. src/linters/lazy_ignores/linter.py +158 -0
  111. src/linters/lazy_ignores/matcher.py +168 -0
  112. src/linters/lazy_ignores/python_analyzer.py +209 -0
  113. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  114. src/linters/lazy_ignores/skip_detector.py +298 -0
  115. src/linters/lazy_ignores/types.py +71 -0
  116. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  117. src/linters/lazy_ignores/violation_builder.py +135 -0
  118. src/linters/lbyl/__init__.py +31 -0
  119. src/linters/lbyl/config.py +63 -0
  120. src/linters/lbyl/linter.py +67 -0
  121. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  122. src/linters/lbyl/pattern_detectors/base.py +63 -0
  123. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  124. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  125. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  126. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  127. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  128. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  129. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  130. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  131. src/linters/lbyl/python_analyzer.py +215 -0
  132. src/linters/lbyl/violation_builder.py +354 -0
  133. src/linters/magic_numbers/__init__.py +48 -0
  134. src/linters/magic_numbers/config.py +82 -0
  135. src/linters/magic_numbers/context_analyzer.py +249 -0
  136. src/linters/magic_numbers/linter.py +462 -0
  137. src/linters/magic_numbers/python_analyzer.py +64 -0
  138. src/linters/magic_numbers/typescript_analyzer.py +215 -0
  139. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  140. src/linters/magic_numbers/violation_builder.py +98 -0
  141. src/linters/method_property/__init__.py +49 -0
  142. src/linters/method_property/config.py +138 -0
  143. src/linters/method_property/linter.py +414 -0
  144. src/linters/method_property/python_analyzer.py +473 -0
  145. src/linters/method_property/violation_builder.py +119 -0
  146. src/linters/nesting/__init__.py +6 -2
  147. src/linters/nesting/config.py +6 -3
  148. src/linters/nesting/linter.py +31 -34
  149. src/linters/nesting/python_analyzer.py +4 -0
  150. src/linters/nesting/typescript_analyzer.py +6 -11
  151. src/linters/nesting/violation_builder.py +1 -0
  152. src/linters/performance/__init__.py +91 -0
  153. src/linters/performance/config.py +43 -0
  154. src/linters/performance/constants.py +49 -0
  155. src/linters/performance/linter.py +149 -0
  156. src/linters/performance/python_analyzer.py +365 -0
  157. src/linters/performance/regex_analyzer.py +312 -0
  158. src/linters/performance/regex_linter.py +139 -0
  159. src/linters/performance/typescript_analyzer.py +236 -0
  160. src/linters/performance/violation_builder.py +160 -0
  161. src/linters/print_statements/__init__.py +53 -0
  162. src/linters/print_statements/config.py +78 -0
  163. src/linters/print_statements/linter.py +413 -0
  164. src/linters/print_statements/python_analyzer.py +153 -0
  165. src/linters/print_statements/typescript_analyzer.py +125 -0
  166. src/linters/print_statements/violation_builder.py +96 -0
  167. src/linters/srp/__init__.py +3 -3
  168. src/linters/srp/class_analyzer.py +11 -7
  169. src/linters/srp/config.py +12 -6
  170. src/linters/srp/heuristics.py +56 -22
  171. src/linters/srp/linter.py +47 -39
  172. src/linters/srp/python_analyzer.py +55 -20
  173. src/linters/srp/typescript_metrics_calculator.py +110 -50
  174. src/linters/stateless_class/__init__.py +25 -0
  175. src/linters/stateless_class/config.py +58 -0
  176. src/linters/stateless_class/linter.py +349 -0
  177. src/linters/stateless_class/python_analyzer.py +290 -0
  178. src/linters/stringly_typed/__init__.py +36 -0
  179. src/linters/stringly_typed/config.py +189 -0
  180. src/linters/stringly_typed/context_filter.py +451 -0
  181. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  182. src/linters/stringly_typed/ignore_checker.py +100 -0
  183. src/linters/stringly_typed/ignore_utils.py +51 -0
  184. src/linters/stringly_typed/linter.py +376 -0
  185. src/linters/stringly_typed/python/__init__.py +33 -0
  186. src/linters/stringly_typed/python/analyzer.py +348 -0
  187. src/linters/stringly_typed/python/call_tracker.py +175 -0
  188. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  189. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  190. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  191. src/linters/stringly_typed/python/constants.py +21 -0
  192. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  193. src/linters/stringly_typed/python/validation_detector.py +189 -0
  194. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  195. src/linters/stringly_typed/storage.py +620 -0
  196. src/linters/stringly_typed/storage_initializer.py +45 -0
  197. src/linters/stringly_typed/typescript/__init__.py +28 -0
  198. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  199. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  200. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  201. src/linters/stringly_typed/violation_generator.py +419 -0
  202. src/orchestrator/core.py +264 -16
  203. src/orchestrator/language_detector.py +5 -3
  204. src/templates/thailint_config_template.yaml +354 -0
  205. src/utils/project_root.py +138 -16
  206. thailint-0.15.3.dist-info/METADATA +187 -0
  207. thailint-0.15.3.dist-info/RECORD +226 -0
  208. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
  209. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  210. src/cli.py +0 -1055
  211. thailint-0.2.0.dist-info/METADATA +0 -980
  212. thailint-0.2.0.dist-info/RECORD +0 -75
  213. thailint-0.2.0.dist-info/entry_points.txt +0 -4
  214. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,96 @@
1
+ """
2
+ Purpose: Builds Violation objects for print statement detection
3
+
4
+ Scope: Violation creation for print and console statement detections
5
+
6
+ Overview: Provides ViolationBuilder class that creates Violation objects for print statement
7
+ detections. Generates descriptive messages suggesting the use of proper logging instead of
8
+ print/console statements. Constructs complete Violation instances with rule_id, file_path,
9
+ line number, column, message, and suggestions. Provides separate methods for Python print()
10
+ violations and TypeScript/JavaScript console.* violations with language-appropriate messages
11
+ and helpful remediation guidance.
12
+
13
+ Dependencies: ast module for Python AST nodes, pathlib.Path for file paths,
14
+ src.core.types.Violation for violation structure
15
+
16
+ Exports: ViolationBuilder class
17
+
18
+ Interfaces: create_python_violation(node, line, file_path) -> Violation,
19
+ create_typescript_violation(method, line, file_path) -> Violation
20
+
21
+ Implementation: Builder pattern with message templates suggesting logging as alternative
22
+ to print/console statements
23
+ """
24
+
25
+ import ast
26
+ from pathlib import Path
27
+
28
+ from src.core.types import Violation
29
+
30
+
31
+ class ViolationBuilder:
32
+ """Builds violations for print statement detections."""
33
+
34
+ def __init__(self, rule_id: str) -> None:
35
+ """Initialize the violation builder.
36
+
37
+ Args:
38
+ rule_id: The rule ID to use in violations
39
+ """
40
+ self.rule_id = rule_id
41
+
42
+ def create_python_violation(
43
+ self,
44
+ node: ast.Call,
45
+ line: int,
46
+ file_path: Path | None,
47
+ ) -> Violation:
48
+ """Create a violation for a Python print() call.
49
+
50
+ Args:
51
+ node: The AST Call node containing the print statement
52
+ line: Line number where the violation occurs
53
+ file_path: Path to the file
54
+
55
+ Returns:
56
+ Violation object with details about the print statement
57
+ """
58
+ message = "print() statement should be replaced with proper logging"
59
+ suggestion = "Use logging.info(), logging.debug(), or similar instead of print()"
60
+
61
+ return Violation(
62
+ rule_id=self.rule_id,
63
+ file_path=str(file_path) if file_path else "",
64
+ line=line,
65
+ column=node.col_offset if hasattr(node, "col_offset") else 0,
66
+ message=message,
67
+ suggestion=suggestion,
68
+ )
69
+
70
+ def create_typescript_violation(
71
+ self,
72
+ method: str,
73
+ line: int,
74
+ file_path: Path | None,
75
+ ) -> Violation:
76
+ """Create a violation for a TypeScript/JavaScript console.* call.
77
+
78
+ Args:
79
+ method: The console method name (log, warn, error, etc.)
80
+ line: Line number where the violation occurs
81
+ file_path: Path to the file
82
+
83
+ Returns:
84
+ Violation object with details about the console statement
85
+ """
86
+ message = f"console.{method}() should be replaced with proper logging"
87
+ suggestion = f"Use a logging library instead of console.{method}()"
88
+
89
+ return Violation(
90
+ rule_id=self.rule_id,
91
+ file_path=str(file_path) if file_path else "",
92
+ line=line,
93
+ column=0, # Tree-sitter nodes don't provide easy column access
94
+ message=message,
95
+ suggestion=suggestion,
96
+ )
@@ -22,7 +22,7 @@ Implementation: Simple re-export pattern for package interface, convenience func
22
22
  from pathlib import Path
23
23
  from typing import Any
24
24
 
25
- from .config import SRPConfig
25
+ from .config import DEFAULT_MAX_LOC_PER_CLASS, DEFAULT_MAX_METHODS_PER_CLASS, SRPConfig
26
26
  from .linter import SRPRule
27
27
  from .python_analyzer import PythonSRPAnalyzer
28
28
  from .typescript_analyzer import TypeScriptSRPAnalyzer
@@ -39,8 +39,8 @@ __all__ = [
39
39
  def lint(
40
40
  path: Path | str,
41
41
  config: dict[str, Any] | None = None,
42
- max_methods: int = 7,
43
- max_loc: int = 200,
42
+ max_methods: int = DEFAULT_MAX_METHODS_PER_CLASS,
43
+ max_loc: int = DEFAULT_MAX_LOC_PER_CLASS,
44
44
  ) -> list:
45
45
  """Lint a file or directory for SRP violations.
46
46
 
@@ -31,6 +31,12 @@ from .typescript_analyzer import TypeScriptSRPAnalyzer
31
31
  class ClassAnalyzer:
32
32
  """Coordinates class analysis for Python and TypeScript."""
33
33
 
34
+ def __init__(self) -> None:
35
+ """Initialize the class analyzer with singleton analyzers."""
36
+ # Singleton analyzers for performance (avoid recreating per-file)
37
+ self._python_analyzer = PythonSRPAnalyzer()
38
+ self._typescript_analyzer = TypeScriptSRPAnalyzer()
39
+
34
40
  def analyze_python(
35
41
  self, context: BaseLintContext, config: SRPConfig
36
42
  ) -> list[dict[str, Any]] | list[Violation]:
@@ -47,10 +53,9 @@ class ClassAnalyzer:
47
53
  if isinstance(tree, list): # Syntax error violations
48
54
  return tree
49
55
 
50
- analyzer = PythonSRPAnalyzer()
51
- classes = analyzer.find_all_classes(tree)
56
+ classes = self._python_analyzer.find_all_classes(tree)
52
57
  return [
53
- analyzer.analyze_class(class_node, context.file_content or "", config)
58
+ self._python_analyzer.analyze_class(class_node, context.file_content or "", config)
54
59
  for class_node in classes
55
60
  ]
56
61
 
@@ -66,14 +71,13 @@ class ClassAnalyzer:
66
71
  Returns:
67
72
  List of class metrics dicts
68
73
  """
69
- analyzer = TypeScriptSRPAnalyzer()
70
- root_node = analyzer.parse_typescript(context.file_content or "")
74
+ root_node = self._typescript_analyzer.parse_typescript(context.file_content or "")
71
75
  if not root_node:
72
76
  return []
73
77
 
74
- classes = analyzer.find_all_classes(root_node)
78
+ classes = self._typescript_analyzer.find_all_classes(root_node)
75
79
  return [
76
- analyzer.analyze_class(class_node, context.file_content or "", config)
80
+ self._typescript_analyzer.analyze_class(class_node, context.file_content or "", config)
77
81
  for class_node in classes
78
82
  ]
79
83
 
src/linters/srp/config.py CHANGED
@@ -23,13 +23,17 @@ Implementation: Dataclass with validation and defaults, heuristic-based SRP dete
23
23
  from dataclasses import dataclass, field
24
24
  from typing import Any
25
25
 
26
+ # Default SRP threshold constants
27
+ DEFAULT_MAX_METHODS_PER_CLASS = 7
28
+ DEFAULT_MAX_LOC_PER_CLASS = 200
29
+
26
30
 
27
31
  @dataclass
28
32
  class SRPConfig:
29
33
  """Configuration for SRP linter."""
30
34
 
31
- max_methods: int = 7 # Maximum methods per class
32
- max_loc: int = 200 # Maximum lines of code per class
35
+ max_methods: int = DEFAULT_MAX_METHODS_PER_CLASS # Maximum methods per class
36
+ max_loc: int = DEFAULT_MAX_LOC_PER_CLASS # Maximum lines of code per class
33
37
  enabled: bool = True
34
38
  check_keywords: bool = True
35
39
  keywords: list[str] = field(
@@ -58,11 +62,13 @@ class SRPConfig:
58
62
  # Get language-specific config if available
59
63
  if language and language in config:
60
64
  lang_config = config[language]
61
- max_methods = lang_config.get("max_methods", config.get("max_methods", 7))
62
- max_loc = lang_config.get("max_loc", config.get("max_loc", 200))
65
+ max_methods = lang_config.get(
66
+ "max_methods", config.get("max_methods", DEFAULT_MAX_METHODS_PER_CLASS)
67
+ )
68
+ max_loc = lang_config.get("max_loc", config.get("max_loc", DEFAULT_MAX_LOC_PER_CLASS))
63
69
  else:
64
- max_methods = config.get("max_methods", 7)
65
- max_loc = config.get("max_loc", 200)
70
+ max_methods = config.get("max_methods", DEFAULT_MAX_METHODS_PER_CLASS)
71
+ max_loc = config.get("max_loc", DEFAULT_MAX_LOC_PER_CLASS)
66
72
 
67
73
  return cls(
68
74
  max_methods=max_methods,
@@ -4,12 +4,13 @@ Purpose: SRP detection heuristics for analyzing code complexity and responsibili
4
4
  Scope: Helper functions for method counting, LOC calculation, and keyword detection
5
5
 
6
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.
7
+ Principle violations. Implements method counting that excludes property decorators,
8
+ private methods, and special methods. Provides LOC calculation that filters out blank
9
+ lines and comments. Includes keyword detection for identifying generic class names that
10
+ often indicate SRP violations (Manager, Handler, etc.). Supports both Python AST and
11
+ TypeScript tree-sitter nodes. These heuristics enable practical SRP detection without
12
+ requiring perfect semantic analysis, focusing on measurable code metrics that correlate
13
+ with responsibility scope.
13
14
 
14
15
  Dependencies: ast module for Python AST analysis, typing for type hints
15
16
 
@@ -24,22 +25,55 @@ import ast
24
25
 
25
26
 
26
27
  def count_methods(class_node: ast.ClassDef) -> int:
27
- """Count methods in a class (excludes properties and special methods).
28
+ """Count public methods in a class (excludes properties and private methods).
29
+
30
+ Private methods are those starting with underscore (_), including dunder
31
+ methods (__init__, __str__, etc.). This focuses SRP analysis on the public
32
+ interface rather than implementation details.
28
33
 
29
34
  Args:
30
35
  class_node: AST node representing a class definition
31
36
 
32
37
  Returns:
33
- Number of methods in the class
38
+ Number of public methods in the class
39
+ """
40
+ func_nodes = (
41
+ n for n in class_node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
42
+ )
43
+ public_methods = [n for n in func_nodes if _is_countable_method(n)]
44
+ return len(public_methods)
45
+
46
+
47
+ def _is_countable_method(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
48
+ """Check if a method should be counted (public and not a property).
49
+
50
+ Args:
51
+ node: Function AST node
52
+
53
+ Returns:
54
+ True if method should be counted
55
+ """
56
+ if has_property_decorator(node):
57
+ return False
58
+ if _is_private_method(node.name):
59
+ return False
60
+ return True
61
+
62
+
63
+ def _is_private_method(method_name: str) -> bool:
64
+ """Check if method is private (starts with underscore).
65
+
66
+ This includes both single underscore (_helper) and dunder methods
67
+ (__init__, __str__). All underscore-prefixed methods are considered
68
+ implementation details.
69
+
70
+ Args:
71
+ method_name: Name of the method to check
72
+
73
+ Returns:
74
+ True if method is private, False otherwise
34
75
  """
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
76
+ return method_name.startswith("_")
43
77
 
44
78
 
45
79
  def count_loc(class_node: ast.ClassDef, source: str) -> int:
@@ -56,8 +90,8 @@ def count_loc(class_node: ast.ClassDef, source: str) -> int:
56
90
  end_line = class_node.end_lineno or start_line
57
91
  lines = source.split("\n")[start_line - 1 : end_line]
58
92
 
59
- # Filter out blank lines and comments
60
- code_lines = [line for line in lines if line.strip() and not line.strip().startswith("#")]
93
+ # Filter out blank lines and comments (using walrus operator to avoid double strip)
94
+ code_lines = [s for line in lines if (s := line.strip()) and not s.startswith("#")]
61
95
  return len(code_lines)
62
96
 
63
97
 
@@ -83,7 +117,7 @@ def has_property_decorator(func_node: ast.FunctionDef | ast.AsyncFunctionDef) ->
83
117
  Returns:
84
118
  True if function has @property decorator
85
119
  """
86
- for decorator in func_node.decorator_list:
87
- if isinstance(decorator, ast.Name) and decorator.id == "property":
88
- return True
89
- return False
120
+ return any(
121
+ isinstance(decorator, ast.Name) and decorator.id == "property"
122
+ for decorator in func_node.decorator_list
123
+ )
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
- from src.core.base import BaseLintContext, BaseLintRule
22
- from src.core.linter_utils import has_file_content, load_linter_config
24
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
25
+ from src.core.constants import Language
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
@@ -29,12 +33,12 @@ from .metrics_evaluator import evaluate_metrics
29
33
  from .violation_builder import ViolationBuilder
30
34
 
31
35
 
32
- class SRPRule(BaseLintRule):
36
+ class SRPRule(MultiLanguageLintRule):
33
37
  """Detects Single Responsibility Principle violations in classes."""
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
 
@@ -54,7 +58,9 @@ class SRPRule(BaseLintRule):
54
58
  return "Classes should have a single, well-defined responsibility"
55
59
 
56
60
  def check(self, context: BaseLintContext) -> list[Violation]:
57
- """Check for SRP violations.
61
+ """Check for SRP violations with custom ignore pattern handling.
62
+
63
+ Overrides parent to add file-level ignore pattern checking before dispatch.
58
64
 
59
65
  Args:
60
66
  context: Lint context with file information
@@ -62,53 +68,60 @@ class SRPRule(BaseLintRule):
62
68
  Returns:
63
69
  List of violations found
64
70
  """
65
- if not self._should_check_file(context):
71
+ from src.core.linter_utils import has_file_content
72
+
73
+ if not has_file_content(context):
66
74
  return []
67
75
 
68
- config = load_linter_config(context, "srp", SRPConfig)
69
- if not self._is_linter_enabled(context, config):
76
+ config = self._load_config(context)
77
+ if not self._should_process_file(context, config):
70
78
  return []
71
79
 
72
- return self._check_by_language(context, config)
80
+ # Standard language dispatch
81
+ return self._dispatch_by_language(context, config)
73
82
 
74
- def _should_check_file(self, context: BaseLintContext) -> bool:
75
- """Check if file has content to analyze.
83
+ def _should_process_file(self, context: BaseLintContext, config: SRPConfig) -> bool:
84
+ """Check if file should be processed.
76
85
 
77
86
  Args:
78
87
  context: Lint context
88
+ config: SRP configuration
79
89
 
80
90
  Returns:
81
- True if file should be checked
91
+ True if file should be processed
82
92
  """
83
- return has_file_content(context)
93
+ if not config.enabled:
94
+ return False
95
+ return not self._is_file_ignored(context, config)
84
96
 
85
- def _is_linter_enabled(self, context: BaseLintContext, config: SRPConfig) -> bool:
86
- """Check if linter is enabled and file is not ignored.
97
+ def _dispatch_by_language(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
98
+ """Dispatch to language-specific checker.
87
99
 
88
100
  Args:
89
101
  context: Lint context
90
102
  config: SRP configuration
91
103
 
92
104
  Returns:
93
- True if linter should run on this file
105
+ List of violations found
94
106
  """
95
- return config.enabled and not self._is_file_ignored(context, config)
107
+ if context.language == Language.PYTHON:
108
+ return self._check_python(context, config)
96
109
 
97
- def _check_by_language(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
98
- """Dispatch to language-specific checker.
110
+ if context.language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
111
+ return self._check_typescript(context, config)
112
+
113
+ return []
114
+
115
+ def _load_config(self, context: BaseLintContext) -> SRPConfig:
116
+ """Load configuration from context.
99
117
 
100
118
  Args:
101
119
  context: Lint context
102
- config: SRP configuration
103
120
 
104
121
  Returns:
105
- List of violations found
122
+ SRPConfig instance
106
123
  """
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 []
124
+ return load_linter_config(context, "srp", SRPConfig)
112
125
 
113
126
  def _is_file_ignored(self, context: BaseLintContext, config: SRPConfig) -> bool:
114
127
  """Check if file matches ignore patterns.
@@ -124,10 +137,7 @@ class SRPRule(BaseLintRule):
124
137
  return False
125
138
 
126
139
  file_path = str(context.file_path)
127
- for pattern in config.ignore:
128
- if pattern in file_path:
129
- return True
130
- return False
140
+ return any(pattern in file_path for pattern in config.ignore)
131
141
 
132
142
  def _check_python(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
133
143
  """Check Python code for SRP violations.
@@ -161,14 +171,12 @@ class SRPRule(BaseLintRule):
161
171
  Returns:
162
172
  List of violations
163
173
  """
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
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
+ ]
172
180
 
173
181
  def _create_violation_if_needed(
174
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)