thailint 0.1.5__py3-none-any.whl → 0.2.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.
- src/__init__.py +7 -2
- src/analyzers/__init__.py +23 -0
- src/analyzers/typescript_base.py +148 -0
- src/api.py +1 -1
- src/cli.py +498 -141
- src/config.py +6 -31
- src/core/base.py +12 -0
- src/core/cli_utils.py +206 -0
- src/core/config_parser.py +99 -0
- src/core/linter_utils.py +168 -0
- src/core/registry.py +17 -92
- src/core/rule_discovery.py +132 -0
- src/core/violation_builder.py +122 -0
- src/linter_config/ignore.py +112 -40
- src/linter_config/loader.py +3 -13
- src/linters/dry/__init__.py +23 -0
- src/linters/dry/base_token_analyzer.py +76 -0
- src/linters/dry/block_filter.py +262 -0
- src/linters/dry/block_grouper.py +59 -0
- src/linters/dry/cache.py +218 -0
- src/linters/dry/cache_query.py +61 -0
- src/linters/dry/config.py +130 -0
- src/linters/dry/config_loader.py +44 -0
- src/linters/dry/deduplicator.py +120 -0
- src/linters/dry/duplicate_storage.py +126 -0
- src/linters/dry/file_analyzer.py +127 -0
- src/linters/dry/inline_ignore.py +140 -0
- src/linters/dry/linter.py +170 -0
- src/linters/dry/python_analyzer.py +517 -0
- src/linters/dry/storage_initializer.py +51 -0
- src/linters/dry/token_hasher.py +115 -0
- src/linters/dry/typescript_analyzer.py +590 -0
- src/linters/dry/violation_builder.py +74 -0
- src/linters/dry/violation_filter.py +91 -0
- src/linters/dry/violation_generator.py +174 -0
- src/linters/file_placement/config_loader.py +86 -0
- src/linters/file_placement/directory_matcher.py +80 -0
- src/linters/file_placement/linter.py +252 -472
- src/linters/file_placement/path_resolver.py +61 -0
- src/linters/file_placement/pattern_matcher.py +55 -0
- src/linters/file_placement/pattern_validator.py +106 -0
- src/linters/file_placement/rule_checker.py +229 -0
- src/linters/file_placement/violation_factory.py +177 -0
- src/linters/nesting/config.py +13 -3
- src/linters/nesting/linter.py +76 -152
- src/linters/nesting/typescript_analyzer.py +38 -102
- src/linters/nesting/typescript_function_extractor.py +130 -0
- src/linters/nesting/violation_builder.py +139 -0
- src/linters/srp/__init__.py +99 -0
- src/linters/srp/class_analyzer.py +113 -0
- src/linters/srp/config.py +76 -0
- src/linters/srp/heuristics.py +89 -0
- src/linters/srp/linter.py +225 -0
- src/linters/srp/metrics_evaluator.py +47 -0
- src/linters/srp/python_analyzer.py +72 -0
- src/linters/srp/typescript_analyzer.py +75 -0
- src/linters/srp/typescript_metrics_calculator.py +90 -0
- src/linters/srp/violation_builder.py +117 -0
- src/orchestrator/core.py +42 -7
- src/utils/__init__.py +4 -0
- src/utils/project_root.py +84 -0
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/METADATA +414 -63
- thailint-0.2.0.dist-info/RECORD +75 -0
- src/.ai/layout.yaml +0 -48
- thailint-0.1.5.dist-info/RECORD +0 -28
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/LICENSE +0 -0
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/WHEEL +0 -0
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Violation creation for nesting depth linter
|
|
3
|
+
|
|
4
|
+
Scope: Builds Violation objects for nesting depth violations
|
|
5
|
+
|
|
6
|
+
Overview: Provides violation building functionality for the nesting depth linter. Creates
|
|
7
|
+
violations for Python and TypeScript functions with excessive nesting, generates contextual
|
|
8
|
+
error messages with actual vs maximum depth, and provides actionable refactoring suggestions
|
|
9
|
+
(early returns, guard clauses, extract method). Handles syntax errors gracefully. Isolates
|
|
10
|
+
violation construction from analysis and checking logic.
|
|
11
|
+
|
|
12
|
+
Dependencies: ast, BaseLintContext, Violation, Severity, NestingConfig, src.core.violation_builder
|
|
13
|
+
|
|
14
|
+
Exports: NestingViolationBuilder
|
|
15
|
+
|
|
16
|
+
Interfaces: create_nesting_violation, create_typescript_nesting_violation, create_syntax_error_violation
|
|
17
|
+
|
|
18
|
+
Implementation: Formats messages with depth information, provides targeted refactoring suggestions,
|
|
19
|
+
extends BaseViolationBuilder for consistent violation construction
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from src.core.base import BaseLintContext
|
|
26
|
+
from src.core.types import Severity, Violation
|
|
27
|
+
from src.core.violation_builder import BaseViolationBuilder
|
|
28
|
+
|
|
29
|
+
from .config import NestingConfig
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NestingViolationBuilder(BaseViolationBuilder):
|
|
33
|
+
"""Builds violations for nesting depth issues."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, rule_id: str):
|
|
36
|
+
"""Initialize violation builder.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
rule_id: Rule identifier for violations
|
|
40
|
+
"""
|
|
41
|
+
self.rule_id = rule_id
|
|
42
|
+
|
|
43
|
+
def create_syntax_error_violation(
|
|
44
|
+
self, error: SyntaxError, context: BaseLintContext
|
|
45
|
+
) -> Violation:
|
|
46
|
+
"""Create violation for syntax error.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
error: SyntaxError exception
|
|
50
|
+
context: Lint context
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Syntax error violation
|
|
54
|
+
"""
|
|
55
|
+
return self.build_from_params(
|
|
56
|
+
rule_id=self.rule_id,
|
|
57
|
+
file_path=str(context.file_path or ""),
|
|
58
|
+
line=error.lineno or 0,
|
|
59
|
+
column=error.offset or 0,
|
|
60
|
+
message=f"Syntax error: {error.msg}",
|
|
61
|
+
severity=Severity.ERROR,
|
|
62
|
+
suggestion="Fix syntax errors before checking nesting depth",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def create_nesting_violation(
|
|
66
|
+
self,
|
|
67
|
+
func: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
68
|
+
max_depth: int,
|
|
69
|
+
config: NestingConfig,
|
|
70
|
+
context: BaseLintContext,
|
|
71
|
+
) -> Violation:
|
|
72
|
+
"""Create violation for excessive nesting in Python function.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
func: Python function AST node
|
|
76
|
+
max_depth: Actual max nesting depth found
|
|
77
|
+
config: Nesting configuration
|
|
78
|
+
context: Lint context
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Nesting depth violation
|
|
82
|
+
"""
|
|
83
|
+
return self.build_from_params(
|
|
84
|
+
rule_id=self.rule_id,
|
|
85
|
+
file_path=str(context.file_path or ""),
|
|
86
|
+
line=func.lineno,
|
|
87
|
+
column=func.col_offset,
|
|
88
|
+
message=f"Function '{func.name}' has excessive nesting depth ({max_depth})",
|
|
89
|
+
severity=Severity.ERROR,
|
|
90
|
+
suggestion=self._generate_suggestion(max_depth, config.max_nesting_depth),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def create_typescript_nesting_violation(
|
|
94
|
+
self,
|
|
95
|
+
func_info: tuple[Any, str],
|
|
96
|
+
max_depth: int,
|
|
97
|
+
config: NestingConfig,
|
|
98
|
+
context: BaseLintContext,
|
|
99
|
+
) -> Violation:
|
|
100
|
+
"""Create violation for excessive nesting in TypeScript function.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
func_info: Tuple of (func_node, func_name)
|
|
104
|
+
max_depth: Actual max nesting depth found
|
|
105
|
+
config: Nesting configuration
|
|
106
|
+
context: Lint context
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Nesting depth violation
|
|
110
|
+
"""
|
|
111
|
+
func_node, func_name = func_info
|
|
112
|
+
line = func_node.start_point[0] + 1 # Convert to 1-indexed
|
|
113
|
+
column = func_node.start_point[1]
|
|
114
|
+
|
|
115
|
+
return self.build_from_params(
|
|
116
|
+
rule_id=self.rule_id,
|
|
117
|
+
file_path=str(context.file_path or ""),
|
|
118
|
+
line=line,
|
|
119
|
+
column=column,
|
|
120
|
+
message=f"Function '{func_name}' has excessive nesting depth ({max_depth})",
|
|
121
|
+
severity=Severity.ERROR,
|
|
122
|
+
suggestion=self._generate_suggestion(max_depth, config.max_nesting_depth),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _generate_suggestion(self, actual_depth: int, max_depth: int) -> str:
|
|
126
|
+
"""Generate refactoring suggestion based on depth.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
actual_depth: Actual nesting depth found
|
|
130
|
+
max_depth: Maximum allowed depth
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Suggestion string with refactoring advice
|
|
134
|
+
"""
|
|
135
|
+
return (
|
|
136
|
+
f"Maximum nesting depth of {actual_depth} exceeds limit of {max_depth}. "
|
|
137
|
+
"Consider extracting nested logic to separate functions, using early returns, "
|
|
138
|
+
"or applying guard clauses to reduce nesting."
|
|
139
|
+
)
|
|
@@ -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 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 = 7,
|
|
43
|
+
max_loc: int = 200,
|
|
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,76 @@
|
|
|
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
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class SRPConfig:
|
|
29
|
+
"""Configuration for SRP linter."""
|
|
30
|
+
|
|
31
|
+
max_methods: int = 7 # Maximum methods per class
|
|
32
|
+
max_loc: int = 200 # Maximum lines of code per class
|
|
33
|
+
enabled: bool = True
|
|
34
|
+
check_keywords: bool = True
|
|
35
|
+
keywords: list[str] = field(
|
|
36
|
+
default_factory=lambda: ["Manager", "Handler", "Processor", "Utility", "Helper"]
|
|
37
|
+
)
|
|
38
|
+
ignore: list[str] = field(default_factory=list) # Path patterns to ignore
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
"""Validate configuration values."""
|
|
42
|
+
if self.max_methods <= 0:
|
|
43
|
+
raise ValueError(f"max_methods must be positive, got {self.max_methods}")
|
|
44
|
+
if self.max_loc <= 0:
|
|
45
|
+
raise ValueError(f"max_loc must be positive, got {self.max_loc}")
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "SRPConfig":
|
|
49
|
+
"""Load configuration from dictionary with language-specific overrides.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
config: Dictionary containing configuration values
|
|
53
|
+
language: Programming language (python, typescript, javascript) for language-specific thresholds
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
SRPConfig instance with values from dictionary
|
|
57
|
+
"""
|
|
58
|
+
# Get language-specific config if available
|
|
59
|
+
if language and language in config:
|
|
60
|
+
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))
|
|
63
|
+
else:
|
|
64
|
+
max_methods = config.get("max_methods", 7)
|
|
65
|
+
max_loc = config.get("max_loc", 200)
|
|
66
|
+
|
|
67
|
+
return cls(
|
|
68
|
+
max_methods=max_methods,
|
|
69
|
+
max_loc=max_loc,
|
|
70
|
+
enabled=config.get("enabled", True),
|
|
71
|
+
check_keywords=config.get("check_keywords", True),
|
|
72
|
+
keywords=config.get(
|
|
73
|
+
"keywords", ["Manager", "Handler", "Processor", "Utility", "Helper"]
|
|
74
|
+
),
|
|
75
|
+
ignore=config.get("ignore", []),
|
|
76
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: SRP detection heuristics for analyzing code complexity and responsibility
|
|
3
|
+
|
|
4
|
+
Scope: Helper functions for method counting, LOC calculation, and keyword detection
|
|
5
|
+
|
|
6
|
+
Overview: Provides heuristic-based analysis functions for detecting Single Responsibility
|
|
7
|
+
Principle violations. Implements method counting that excludes property decorators and
|
|
8
|
+
special methods. Provides LOC calculation that filters out blank lines and comments.
|
|
9
|
+
Includes keyword detection for identifying generic class names that often indicate SRP
|
|
10
|
+
violations (Manager, Handler, etc.). Supports both Python AST and TypeScript tree-sitter
|
|
11
|
+
nodes. These heuristics enable practical SRP detection without requiring perfect semantic
|
|
12
|
+
analysis, focusing on measurable code metrics that correlate with responsibility scope.
|
|
13
|
+
|
|
14
|
+
Dependencies: ast module for Python AST analysis, typing for type hints
|
|
15
|
+
|
|
16
|
+
Exports: count_methods, count_loc, has_responsibility_keyword, has_property_decorator
|
|
17
|
+
|
|
18
|
+
Interfaces: Functions accepting AST nodes and returning metrics (int, bool)
|
|
19
|
+
|
|
20
|
+
Implementation: AST walking with filtering logic, heuristic-based thresholds
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def count_methods(class_node: ast.ClassDef) -> int:
|
|
27
|
+
"""Count methods in a class (excludes properties and special methods).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
class_node: AST node representing a class definition
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Number of methods in the class
|
|
34
|
+
"""
|
|
35
|
+
methods = 0
|
|
36
|
+
for node in class_node.body:
|
|
37
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
38
|
+
continue
|
|
39
|
+
# Don't count @property decorators as methods
|
|
40
|
+
if not has_property_decorator(node):
|
|
41
|
+
methods += 1
|
|
42
|
+
return methods
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def count_loc(class_node: ast.ClassDef, source: str) -> int:
|
|
46
|
+
"""Count lines of code in a class (excludes blank lines and comments).
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
class_node: AST node representing a class definition
|
|
50
|
+
source: Full source code of the file
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Number of code lines in the class
|
|
54
|
+
"""
|
|
55
|
+
start_line = class_node.lineno
|
|
56
|
+
end_line = class_node.end_lineno or start_line
|
|
57
|
+
lines = source.split("\n")[start_line - 1 : end_line]
|
|
58
|
+
|
|
59
|
+
# Filter out blank lines and comments
|
|
60
|
+
code_lines = [line for line in lines if line.strip() and not line.strip().startswith("#")]
|
|
61
|
+
return len(code_lines)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def has_responsibility_keyword(class_name: str, keywords: list[str]) -> bool:
|
|
65
|
+
"""Check if class name contains responsibility keywords.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
class_name: Name of the class to check
|
|
69
|
+
keywords: List of keywords indicating potential SRP violations
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if class name contains any responsibility keyword
|
|
73
|
+
"""
|
|
74
|
+
return any(keyword in class_name for keyword in keywords)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def has_property_decorator(func_node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
78
|
+
"""Check if function has @property decorator.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
func_node: AST node representing a function definition
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if function has @property decorator
|
|
85
|
+
"""
|
|
86
|
+
for decorator in func_node.decorator_list:
|
|
87
|
+
if isinstance(decorator, ast.Name) and decorator.id == "property":
|
|
88
|
+
return True
|
|
89
|
+
return False
|