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,155 @@
1
+ """
2
+ File: src/linters/print_statements/python_analyzer.py
3
+
4
+ Purpose: Python AST analysis for finding print() call nodes
5
+
6
+ Exports: PythonPrintStatementAnalyzer class
7
+
8
+ Depends: ast module for AST parsing and node types
9
+
10
+ Implements: PythonPrintStatementAnalyzer.find_print_calls(tree) -> list[tuple],
11
+ PythonPrintStatementAnalyzer.is_in_main_block(node) -> bool
12
+
13
+ Related: src/linters/magic_numbers/python_analyzer.py
14
+
15
+ Overview: Provides PythonPrintStatementAnalyzer class that traverses Python AST to find all
16
+ print() function calls. Uses ast.walk() to traverse the syntax tree and collect
17
+ Call nodes where the function is 'print'. Tracks parent nodes to detect if print calls
18
+ are within __main__ blocks (if __name__ == "__main__":) for allow_in_scripts filtering.
19
+ Returns structured data about each print call including the AST node, parent context,
20
+ and line number for violation reporting.
21
+
22
+ Usage: analyzer = PythonPrintStatementAnalyzer()
23
+ print_calls = analyzer.find_print_calls(ast.parse(code))
24
+
25
+ Notes: AST walk pattern with parent tracking for context detection
26
+ """
27
+
28
+ import ast
29
+
30
+
31
+ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
32
+ """Analyzes Python AST to find print() calls."""
33
+
34
+ def __init__(self) -> None:
35
+ """Initialize the analyzer."""
36
+ self.print_calls: list[tuple[ast.Call, ast.AST | None, int]] = []
37
+ self.parent_map: dict[ast.AST, ast.AST] = {}
38
+
39
+ def find_print_calls(self, tree: ast.AST) -> list[tuple[ast.Call, ast.AST | None, int]]:
40
+ """Find all print() calls in the AST.
41
+
42
+ Args:
43
+ tree: The AST to analyze
44
+
45
+ Returns:
46
+ List of tuples (node, parent, line_number)
47
+ """
48
+ self.print_calls = []
49
+ self.parent_map = {}
50
+ self._build_parent_map(tree)
51
+ self._collect_print_calls(tree)
52
+ return self.print_calls
53
+
54
+ def _build_parent_map(self, node: ast.AST, parent: ast.AST | None = None) -> None:
55
+ """Build a map of nodes to their parents.
56
+
57
+ Args:
58
+ node: Current AST node
59
+ parent: Parent of current node
60
+ """
61
+ if parent is not None:
62
+ self.parent_map[node] = parent
63
+
64
+ for child in ast.iter_child_nodes(node):
65
+ self._build_parent_map(child, node)
66
+
67
+ def _collect_print_calls(self, tree: ast.AST) -> None:
68
+ """Walk tree and collect all print() calls.
69
+
70
+ Args:
71
+ tree: AST to traverse
72
+ """
73
+ for node in ast.walk(tree):
74
+ if isinstance(node, ast.Call) and self._is_print_call(node):
75
+ parent = self.parent_map.get(node)
76
+ line_number = node.lineno if hasattr(node, "lineno") else 0
77
+ self.print_calls.append((node, parent, line_number))
78
+
79
+ def _is_print_call(self, node: ast.Call) -> bool:
80
+ """Check if a Call node is calling print().
81
+
82
+ Args:
83
+ node: The Call node to check
84
+
85
+ Returns:
86
+ True if this is a print() call
87
+ """
88
+ return self._is_simple_print(node) or self._is_builtins_print(node)
89
+
90
+ def _is_simple_print(self, node: ast.Call) -> bool:
91
+ """Check for simple print() call."""
92
+ return isinstance(node.func, ast.Name) and node.func.id == "print"
93
+
94
+ def _is_builtins_print(self, node: ast.Call) -> bool:
95
+ """Check for builtins.print() call."""
96
+ if not isinstance(node.func, ast.Attribute):
97
+ return False
98
+ if node.func.attr != "print":
99
+ return False
100
+ return isinstance(node.func.value, ast.Name) and node.func.value.id == "builtins"
101
+
102
+ def is_in_main_block(self, node: ast.AST) -> bool:
103
+ """Check if node is within `if __name__ == "__main__":` block.
104
+
105
+ Args:
106
+ node: AST node to check
107
+
108
+ Returns:
109
+ True if node is inside a __main__ block
110
+ """
111
+ current = node
112
+ while current in self.parent_map:
113
+ parent = self.parent_map[current]
114
+ if self._is_main_if_block(parent):
115
+ return True
116
+ current = parent
117
+ return False
118
+
119
+ def _is_main_if_block(self, node: ast.AST) -> bool:
120
+ """Check if node is an `if __name__ == "__main__":` statement.
121
+
122
+ Args:
123
+ node: AST node to check
124
+
125
+ Returns:
126
+ True if this is a __main__ if block
127
+ """
128
+ if not isinstance(node, ast.If):
129
+ return False
130
+ if not isinstance(node.test, ast.Compare):
131
+ return False
132
+ return self._is_main_comparison(node.test)
133
+
134
+ def _is_main_comparison(self, test: ast.Compare) -> bool:
135
+ """Check if comparison is __name__ == '__main__'."""
136
+ if not self._is_name_identifier(test.left):
137
+ return False
138
+ if not self._has_single_eq_operator(test):
139
+ return False
140
+ return self._compares_to_main(test)
141
+
142
+ def _is_name_identifier(self, node: ast.expr) -> bool:
143
+ """Check if node is the __name__ identifier."""
144
+ return isinstance(node, ast.Name) and node.id == "__name__"
145
+
146
+ def _has_single_eq_operator(self, test: ast.Compare) -> bool:
147
+ """Check if comparison has single == operator."""
148
+ return len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)
149
+
150
+ def _compares_to_main(self, test: ast.Compare) -> bool:
151
+ """Check if comparison is to '__main__' string."""
152
+ if len(test.comparators) != 1:
153
+ return False
154
+ comparator = test.comparators[0]
155
+ return isinstance(comparator, ast.Constant) and comparator.value == "__main__"
@@ -0,0 +1,135 @@
1
+ """
2
+ File: src/linters/print_statements/typescript_analyzer.py
3
+
4
+ Purpose: TypeScript/JavaScript console.* call detection using Tree-sitter AST analysis
5
+
6
+ Exports: TypeScriptPrintStatementAnalyzer class
7
+
8
+ Depends: TypeScriptBaseAnalyzer for tree-sitter parsing, tree-sitter Node type
9
+
10
+ Implements: find_console_calls(root_node, methods) -> list[tuple]
11
+
12
+ Related: src/linters/magic_numbers/typescript_analyzer.py, src/analyzers/typescript_base.py
13
+
14
+ Overview: Analyzes TypeScript and JavaScript code to detect console.* method calls that should
15
+ be replaced with proper logging. Uses Tree-sitter parser to traverse TypeScript/JavaScript
16
+ AST and identify call expressions where the callee is console.log, console.warn, console.error,
17
+ console.debug, or console.info (configurable). Returns structured data with the node, method
18
+ name, and line number for each detected console call. Supports both TypeScript and JavaScript
19
+ files with shared detection logic.
20
+
21
+ Usage: analyzer = TypeScriptPrintStatementAnalyzer()
22
+ root = analyzer.parse_typescript(code)
23
+ calls = analyzer.find_console_calls(root, {"log", "warn", "error"})
24
+
25
+ Notes: Tree-sitter node traversal with call_expression and member_expression pattern matching
26
+ """
27
+
28
+ import logging
29
+ from typing import Any
30
+
31
+ from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
36
+ try:
37
+ from tree_sitter import Node
38
+
39
+ TREE_SITTER_AVAILABLE = True
40
+ except ImportError:
41
+ TREE_SITTER_AVAILABLE = False
42
+ Node = Any # type: ignore
43
+
44
+
45
+ class TypeScriptPrintStatementAnalyzer(TypeScriptBaseAnalyzer):
46
+ """Analyzes TypeScript/JavaScript code for console.* calls using Tree-sitter."""
47
+
48
+ def find_console_calls(self, root_node: Node, methods: set[str]) -> list[tuple[Node, str, int]]:
49
+ """Find all console.* calls matching the specified methods.
50
+
51
+ Args:
52
+ root_node: Root tree-sitter node to search from
53
+ methods: Set of console method names to detect (e.g., {"log", "warn"})
54
+
55
+ Returns:
56
+ List of (node, method_name, line_number) tuples for each console call
57
+ """
58
+ logger.debug(
59
+ "find_console_calls: TREE_SITTER_AVAILABLE=%s, root_node=%s",
60
+ TREE_SITTER_AVAILABLE,
61
+ root_node is not None,
62
+ )
63
+ if not TREE_SITTER_AVAILABLE or root_node is None:
64
+ logger.debug("Early return: tree-sitter not available or root_node is None")
65
+ return []
66
+
67
+ calls: list[tuple[Node, str, int]] = []
68
+ self._collect_console_calls(root_node, methods, calls)
69
+ logger.debug("find_console_calls: found %d calls", len(calls))
70
+ return calls
71
+
72
+ def _collect_console_calls(
73
+ self, node: Node, methods: set[str], calls: list[tuple[Node, str, int]]
74
+ ) -> None:
75
+ """Recursively collect console.* calls from AST.
76
+
77
+ Args:
78
+ node: Current tree-sitter node
79
+ methods: Set of console method names to detect
80
+ calls: List to accumulate found calls
81
+ """
82
+ if node.type == "call_expression":
83
+ method_name = self._extract_console_method(node, methods)
84
+ if method_name is not None:
85
+ line_number = node.start_point[0] + 1
86
+ calls.append((node, method_name, line_number))
87
+
88
+ for child in node.children:
89
+ self._collect_console_calls(child, methods, calls)
90
+
91
+ def _extract_console_method(self, node: Node, methods: set[str]) -> str | None:
92
+ """Extract console method name if this is a console.* call.
93
+
94
+ Args:
95
+ node: Tree-sitter call_expression node
96
+ methods: Set of console method names to detect
97
+
98
+ Returns:
99
+ Method name if this is a matching console call, None otherwise
100
+ """
101
+ func_node = self.find_child_by_type(node, "member_expression")
102
+ if func_node is None:
103
+ return None
104
+ if not self._is_console_object(func_node):
105
+ return None
106
+ return self._get_matching_method(func_node, methods)
107
+
108
+ def _is_console_object(self, func_node: Node) -> bool:
109
+ """Check if the member expression is on 'console' object."""
110
+ object_node = self._find_object_node(func_node)
111
+ if object_node is None:
112
+ return False
113
+ return self.extract_node_text(object_node) == "console"
114
+
115
+ def _get_matching_method(self, func_node: Node, methods: set[str]) -> str | None:
116
+ """Get method name if it matches the configured methods."""
117
+ method_node = self.find_child_by_type(func_node, "property_identifier")
118
+ if method_node is None:
119
+ return None
120
+ method_name = self.extract_node_text(method_node)
121
+ return method_name if method_name in methods else None
122
+
123
+ def _find_object_node(self, member_expr: Node) -> Node | None:
124
+ """Find the object node in a member expression.
125
+
126
+ Args:
127
+ member_expr: Tree-sitter member_expression node
128
+
129
+ Returns:
130
+ Object node (identifier) or None
131
+ """
132
+ for child in member_expr.children:
133
+ if child.type == "identifier":
134
+ return child
135
+ return None
@@ -0,0 +1,98 @@
1
+ """
2
+ File: src/linters/print_statements/violation_builder.py
3
+
4
+ Purpose: Builds Violation objects for print statement detection
5
+
6
+ Exports: ViolationBuilder class
7
+
8
+ Depends: ast, pathlib.Path, src.core.types.Violation
9
+
10
+ Implements: ViolationBuilder.create_python_violation(node, line, file_path) -> Violation,
11
+ ViolationBuilder.create_typescript_violation(method, line, file_path) -> Violation
12
+
13
+ Related: src/linters/magic_numbers/violation_builder.py, src/core/types.py
14
+
15
+ Overview: Provides ViolationBuilder class that creates Violation objects for print statement
16
+ detections. Generates descriptive messages suggesting the use of proper logging instead of
17
+ print/console statements. Constructs complete Violation instances with rule_id, file_path,
18
+ line number, column, message, and suggestions. Provides separate methods for Python print()
19
+ violations and TypeScript/JavaScript console.* violations with language-appropriate messages.
20
+
21
+ Usage: builder = ViolationBuilder("print-statements.detected")
22
+ violation = builder.create_python_violation(node, line, file_path)
23
+
24
+ Notes: Message templates suggest logging as alternative, consistent with other linter patterns
25
+ """
26
+
27
+ import ast
28
+ from pathlib import Path
29
+
30
+ from src.core.types import Violation
31
+
32
+
33
+ class ViolationBuilder:
34
+ """Builds violations for print statement detections."""
35
+
36
+ def __init__(self, rule_id: str) -> None:
37
+ """Initialize the violation builder.
38
+
39
+ Args:
40
+ rule_id: The rule ID to use in violations
41
+ """
42
+ self.rule_id = rule_id
43
+
44
+ def create_python_violation(
45
+ self,
46
+ node: ast.Call,
47
+ line: int,
48
+ file_path: Path | None,
49
+ ) -> Violation:
50
+ """Create a violation for a Python print() call.
51
+
52
+ Args:
53
+ node: The AST Call node containing the print statement
54
+ line: Line number where the violation occurs
55
+ file_path: Path to the file
56
+
57
+ Returns:
58
+ Violation object with details about the print statement
59
+ """
60
+ message = "print() statement should be replaced with proper logging"
61
+ suggestion = "Use logging.info(), logging.debug(), or similar instead of print()"
62
+
63
+ return Violation(
64
+ rule_id=self.rule_id,
65
+ file_path=str(file_path) if file_path else "",
66
+ line=line,
67
+ column=node.col_offset if hasattr(node, "col_offset") else 0,
68
+ message=message,
69
+ suggestion=suggestion,
70
+ )
71
+
72
+ def create_typescript_violation(
73
+ self,
74
+ method: str,
75
+ line: int,
76
+ file_path: Path | None,
77
+ ) -> Violation:
78
+ """Create a violation for a TypeScript/JavaScript console.* call.
79
+
80
+ Args:
81
+ method: The console method name (log, warn, error, etc.)
82
+ line: Line number where the violation occurs
83
+ file_path: Path to the file
84
+
85
+ Returns:
86
+ Violation object with details about the console statement
87
+ """
88
+ message = f"console.{method}() should be replaced with proper logging"
89
+ suggestion = f"Use a logging library instead of console.{method}()"
90
+
91
+ return Violation(
92
+ rule_id=self.rule_id,
93
+ file_path=str(file_path) if file_path else "",
94
+ line=line,
95
+ column=0, # Tree-sitter nodes don't provide easy column access
96
+ message=message,
97
+ suggestion=suggestion,
98
+ )
@@ -0,0 +1,99 @@
1
+ """
2
+ Purpose: SRP linter package initialization
3
+
4
+ Scope: Exports for Single Responsibility Principle linter module
5
+
6
+ Overview: Initializes the SRP linter package and exposes the main rule class for external use.
7
+ Exports SRPRule as the primary interface for the SRP linter, allowing the orchestrator to
8
+ discover and instantiate the rule. Also exports configuration and analyzer classes for
9
+ advanced use cases. Provides a convenience lint() function for direct usage without
10
+ orchestrator setup. This module serves as the entry point for the SRP linter functionality
11
+ within the thai-lint framework, enabling detection of classes with too many responsibilities.
12
+
13
+ Dependencies: SRPRule, SRPConfig, PythonSRPAnalyzer, TypeScriptSRPAnalyzer
14
+
15
+ Exports: SRPRule (primary), SRPConfig, PythonSRPAnalyzer, TypeScriptSRPAnalyzer, lint
16
+
17
+ Interfaces: Standard Python package initialization with __all__ for explicit exports, lint() convenience function
18
+
19
+ Implementation: Simple re-export pattern for package interface, convenience function wraps orchestrator
20
+ """
21
+
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from .config import DEFAULT_MAX_LOC_PER_CLASS, DEFAULT_MAX_METHODS_PER_CLASS, SRPConfig
26
+ from .linter import SRPRule
27
+ from .python_analyzer import PythonSRPAnalyzer
28
+ from .typescript_analyzer import TypeScriptSRPAnalyzer
29
+
30
+ __all__ = [
31
+ "SRPRule",
32
+ "SRPConfig",
33
+ "PythonSRPAnalyzer",
34
+ "TypeScriptSRPAnalyzer",
35
+ "lint",
36
+ ]
37
+
38
+
39
+ def lint(
40
+ path: Path | str,
41
+ config: dict[str, Any] | None = None,
42
+ max_methods: int = DEFAULT_MAX_METHODS_PER_CLASS,
43
+ max_loc: int = DEFAULT_MAX_LOC_PER_CLASS,
44
+ ) -> list:
45
+ """Lint a file or directory for SRP violations.
46
+
47
+ Args:
48
+ path: Path to file or directory to lint
49
+ config: Configuration dict (optional, uses defaults if not provided)
50
+ max_methods: Maximum allowed methods per class (default: 7)
51
+ max_loc: Maximum allowed lines of code per class (default: 200)
52
+
53
+ Returns:
54
+ List of violations found
55
+
56
+ Example:
57
+ >>> from src.linters.srp import lint
58
+ >>> violations = lint('src/my_module.py', max_methods=5)
59
+ >>> for v in violations:
60
+ ... print(f"{v.file_path}:{v.line} - {v.message}")
61
+ """
62
+ path_obj = Path(path) if isinstance(path, str) else path
63
+ project_root = path_obj if path_obj.is_dir() else path_obj.parent
64
+
65
+ orchestrator = _setup_srp_orchestrator(project_root, config, max_methods, max_loc)
66
+ violations = _execute_srp_lint(orchestrator, path_obj)
67
+
68
+ return [v for v in violations if "srp" in v.rule_id]
69
+
70
+
71
+ def _setup_srp_orchestrator(
72
+ project_root: Path,
73
+ config: dict[str, Any] | None,
74
+ max_methods: int,
75
+ max_loc: int,
76
+ ) -> Any:
77
+ """Set up orchestrator with SRP config."""
78
+ from src.orchestrator.core import Orchestrator
79
+
80
+ orchestrator = Orchestrator(project_root=project_root)
81
+
82
+ if config:
83
+ orchestrator.config["srp"] = config
84
+ else:
85
+ orchestrator.config["srp"] = {
86
+ "max_methods": max_methods,
87
+ "max_loc": max_loc,
88
+ }
89
+
90
+ return orchestrator
91
+
92
+
93
+ def _execute_srp_lint(orchestrator: Any, path_obj: Path) -> list:
94
+ """Execute linting on file or directory."""
95
+ if path_obj.is_file():
96
+ return orchestrator.lint_file(path_obj)
97
+ if path_obj.is_dir():
98
+ return orchestrator.lint_directory(path_obj)
99
+ return []
@@ -0,0 +1,113 @@
1
+ """
2
+ Purpose: Class analysis coordination for SRP linter
3
+
4
+ Scope: Coordinates Python and TypeScript class analysis
5
+
6
+ Overview: Provides unified class analysis interface for the SRP linter. Delegates to language-
7
+ specific analyzers (PythonSRPAnalyzer, TypeScriptSRPAnalyzer) based on language type.
8
+ Handles syntax error gracefully and extracts class metrics for SRP evaluation. Isolates
9
+ language-specific analysis logic from rule checking and violation building.
10
+
11
+ Dependencies: ast, PythonSRPAnalyzer, TypeScriptSRPAnalyzer, BaseLintContext, SRPConfig
12
+
13
+ Exports: ClassAnalyzer
14
+
15
+ Interfaces: analyze_python(context, config) -> list[dict], analyze_typescript(context, config) -> list[dict]
16
+
17
+ Implementation: Delegates to language-specific analyzers, returns normalized metrics dicts
18
+ """
19
+
20
+ import ast
21
+ from typing import Any
22
+
23
+ from src.core.base import BaseLintContext
24
+ from src.core.types import Severity, Violation
25
+
26
+ from .config import SRPConfig
27
+ from .python_analyzer import PythonSRPAnalyzer
28
+ from .typescript_analyzer import TypeScriptSRPAnalyzer
29
+
30
+
31
+ class ClassAnalyzer:
32
+ """Coordinates class analysis for Python and TypeScript."""
33
+
34
+ def analyze_python(
35
+ self, context: BaseLintContext, config: SRPConfig
36
+ ) -> list[dict[str, Any]] | list[Violation]:
37
+ """Analyze Python classes and return metrics or syntax errors.
38
+
39
+ Args:
40
+ context: Lint context with file information
41
+ config: SRP configuration
42
+
43
+ Returns:
44
+ List of class metrics dicts, or list of syntax error violations
45
+ """
46
+ tree = self._parse_python_safely(context)
47
+ if isinstance(tree, list): # Syntax error violations
48
+ return tree
49
+
50
+ analyzer = PythonSRPAnalyzer()
51
+ classes = analyzer.find_all_classes(tree)
52
+ return [
53
+ analyzer.analyze_class(class_node, context.file_content or "", config)
54
+ for class_node in classes
55
+ ]
56
+
57
+ def analyze_typescript(
58
+ self, context: BaseLintContext, config: SRPConfig
59
+ ) -> list[dict[str, Any]]:
60
+ """Analyze TypeScript classes and return metrics.
61
+
62
+ Args:
63
+ context: Lint context with file information
64
+ config: SRP configuration
65
+
66
+ Returns:
67
+ List of class metrics dicts
68
+ """
69
+ analyzer = TypeScriptSRPAnalyzer()
70
+ root_node = analyzer.parse_typescript(context.file_content or "")
71
+ if not root_node:
72
+ return []
73
+
74
+ classes = analyzer.find_all_classes(root_node)
75
+ return [
76
+ analyzer.analyze_class(class_node, context.file_content or "", config)
77
+ for class_node in classes
78
+ ]
79
+
80
+ def _parse_python_safely(self, context: BaseLintContext) -> ast.AST | list[Violation]:
81
+ """Parse Python code and return AST or syntax error violations.
82
+
83
+ Args:
84
+ context: Lint context with file information
85
+
86
+ Returns:
87
+ AST if successful, list of syntax error violations otherwise
88
+ """
89
+ try:
90
+ return ast.parse(context.file_content or "")
91
+ except SyntaxError as exc:
92
+ return [self._create_syntax_error_violation(exc, context)]
93
+
94
+ def _create_syntax_error_violation(
95
+ self, exc: SyntaxError, context: BaseLintContext
96
+ ) -> Violation:
97
+ """Create syntax error violation.
98
+
99
+ Args:
100
+ exc: SyntaxError exception
101
+ context: Lint context
102
+
103
+ Returns:
104
+ Syntax error violation
105
+ """
106
+ return Violation(
107
+ rule_id="srp.syntax-error",
108
+ file_path=str(context.file_path or ""),
109
+ line=exc.lineno or 1,
110
+ column=exc.offset or 0,
111
+ message=f"Syntax error: {exc.msg}",
112
+ severity=Severity.ERROR,
113
+ )
@@ -0,0 +1,82 @@
1
+ """
2
+ Purpose: Configuration schema for Single Responsibility Principle linter
3
+
4
+ Scope: SRPConfig dataclass with max_methods, max_loc, and keyword settings
5
+
6
+ Overview: Defines configuration schema for SRP linter. Provides SRPConfig dataclass with
7
+ max_methods field (default 7), max_loc field (default 200), and check_keywords flag
8
+ (default True) with configurable responsibility keywords. Supports per-file and
9
+ per-directory config overrides. Validates that thresholds are positive integers.
10
+ Integrates with the orchestrator's configuration system to allow users to customize
11
+ SRP thresholds via .thailint.yaml configuration files. Keywords list identifies
12
+ generic class names that often indicate SRP violations (Manager, Handler, etc.).
13
+
14
+ Dependencies: dataclasses, typing
15
+
16
+ Exports: SRPConfig dataclass
17
+
18
+ Interfaces: SRPConfig(max_methods, max_loc, check_keywords, keywords), from_dict class method
19
+
20
+ Implementation: Dataclass with validation and defaults, heuristic-based SRP detection thresholds
21
+ """
22
+
23
+ from dataclasses import dataclass, field
24
+ from typing import Any
25
+
26
+ # Default SRP threshold constants
27
+ DEFAULT_MAX_METHODS_PER_CLASS = 7
28
+ DEFAULT_MAX_LOC_PER_CLASS = 200
29
+
30
+
31
+ @dataclass
32
+ class SRPConfig:
33
+ """Configuration for SRP linter."""
34
+
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
37
+ enabled: bool = True
38
+ check_keywords: bool = True
39
+ keywords: list[str] = field(
40
+ default_factory=lambda: ["Manager", "Handler", "Processor", "Utility", "Helper"]
41
+ )
42
+ ignore: list[str] = field(default_factory=list) # Path patterns to ignore
43
+
44
+ def __post_init__(self) -> None:
45
+ """Validate configuration values."""
46
+ if self.max_methods <= 0:
47
+ raise ValueError(f"max_methods must be positive, got {self.max_methods}")
48
+ if self.max_loc <= 0:
49
+ raise ValueError(f"max_loc must be positive, got {self.max_loc}")
50
+
51
+ @classmethod
52
+ def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "SRPConfig":
53
+ """Load configuration from dictionary with language-specific overrides.
54
+
55
+ Args:
56
+ config: Dictionary containing configuration values
57
+ language: Programming language (python, typescript, javascript) for language-specific thresholds
58
+
59
+ Returns:
60
+ SRPConfig instance with values from dictionary
61
+ """
62
+ # Get language-specific config if available
63
+ if language and language in config:
64
+ lang_config = config[language]
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))
69
+ else:
70
+ max_methods = config.get("max_methods", DEFAULT_MAX_METHODS_PER_CLASS)
71
+ max_loc = config.get("max_loc", DEFAULT_MAX_LOC_PER_CLASS)
72
+
73
+ return cls(
74
+ max_methods=max_methods,
75
+ max_loc=max_loc,
76
+ enabled=config.get("enabled", True),
77
+ check_keywords=config.get("check_keywords", True),
78
+ keywords=config.get(
79
+ "keywords", ["Manager", "Handler", "Processor", "Utility", "Helper"]
80
+ ),
81
+ ignore=config.get("ignore", []),
82
+ )