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.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +44 -27
- src/core/base.py +95 -5
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +36 -6
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +125 -22
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +142 -94
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +68 -21
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +20 -82
- src/linters/dry/file_analyzer.py +15 -50
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +182 -54
- src/linters/dry/python_analyzer.py +108 -336
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/token_hasher.py +129 -71
- src/linters/dry/typescript_analyzer.py +68 -380
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +9 -5
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +105 -0
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +140 -0
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +72 -0
- src/linters/file_header/linter.py +309 -0
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +42 -0
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +79 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +74 -31
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +249 -0
- src/linters/magic_numbers/linter.py +462 -0
- src/linters/magic_numbers/python_analyzer.py +64 -0
- src/linters/magic_numbers/typescript_analyzer.py +215 -0
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +6 -3
- src/linters/nesting/linter.py +31 -34
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -11
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +78 -0
- src/linters/print_statements/linter.py +413 -0
- src/linters/print_statements/python_analyzer.py +153 -0
- src/linters/print_statements/typescript_analyzer.py +125 -0
- src/linters/print_statements/violation_builder.py +96 -0
- src/linters/srp/__init__.py +3 -3
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/config.py +12 -6
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +47 -39
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +264 -16
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +354 -0
- src/utils/project_root.py +138 -16
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1055
- thailint-0.2.0.dist-info/METADATA +0 -980
- thailint-0.2.0.dist-info/RECORD +0 -75
- thailint-0.2.0.dist-info/entry_points.txt +0 -4
- {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
|
+
)
|
src/linters/srp/__init__.py
CHANGED
|
@@ -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 =
|
|
43
|
-
max_loc: int =
|
|
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
|
-
|
|
51
|
-
classes = analyzer.find_all_classes(tree)
|
|
56
|
+
classes = self._python_analyzer.find_all_classes(tree)
|
|
52
57
|
return [
|
|
53
|
-
|
|
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
|
-
|
|
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 =
|
|
78
|
+
classes = self._typescript_analyzer.find_all_classes(root_node)
|
|
75
79
|
return [
|
|
76
|
-
|
|
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 =
|
|
32
|
-
max_loc: int =
|
|
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(
|
|
62
|
-
|
|
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",
|
|
65
|
-
max_loc = config.get("max_loc",
|
|
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,
|
src/linters/srp/heuristics.py
CHANGED
|
@@ -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
|
|
8
|
-
special methods. Provides LOC calculation that filters out blank
|
|
9
|
-
Includes keyword detection for identifying generic class names that
|
|
10
|
-
violations (Manager, Handler, etc.). Supports both Python AST and
|
|
11
|
-
nodes. These heuristics enable practical SRP detection without
|
|
12
|
-
analysis, focusing on measurable code metrics that correlate
|
|
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
|
|
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
|
-
|
|
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 = [
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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,
|
|
22
|
-
from src.core.
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
69
|
-
if not self.
|
|
76
|
+
config = self._load_config(context)
|
|
77
|
+
if not self._should_process_file(context, config):
|
|
70
78
|
return []
|
|
71
79
|
|
|
72
|
-
|
|
80
|
+
# Standard language dispatch
|
|
81
|
+
return self._dispatch_by_language(context, config)
|
|
73
82
|
|
|
74
|
-
def
|
|
75
|
-
"""Check if file
|
|
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
|
|
91
|
+
True if file should be processed
|
|
82
92
|
"""
|
|
83
|
-
|
|
93
|
+
if not config.enabled:
|
|
94
|
+
return False
|
|
95
|
+
return not self._is_file_ignored(context, config)
|
|
84
96
|
|
|
85
|
-
def
|
|
86
|
-
"""
|
|
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
|
-
|
|
105
|
+
List of violations found
|
|
94
106
|
"""
|
|
95
|
-
|
|
107
|
+
if context.language == Language.PYTHON:
|
|
108
|
+
return self._check_python(context, config)
|
|
96
109
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
122
|
+
SRPConfig instance
|
|
106
123
|
"""
|
|
107
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
violation
|
|
169
|
-
|
|
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:
|
|
4
|
+
Scope: Functions for analyzing Python classes using AST
|
|
5
5
|
|
|
6
6
|
Overview: Implements Python-specific SRP analysis using the ast module to parse and analyze
|
|
7
7
|
class definitions. Walks the AST to find all class definitions, then analyzes each class
|
|
@@ -13,7 +13,7 @@ Overview: Implements Python-specific SRP analysis using the ast module to parse
|
|
|
13
13
|
|
|
14
14
|
Dependencies: ast module for Python AST parsing, typing for type hints, heuristics module
|
|
15
15
|
|
|
16
|
-
Exports: PythonSRPAnalyzer class
|
|
16
|
+
Exports: find_all_classes function, analyze_class function, PythonSRPAnalyzer class (compat)
|
|
17
17
|
|
|
18
18
|
Interfaces: find_all_classes(tree), analyze_class(class_node, source, config)
|
|
19
19
|
|
|
@@ -27,8 +27,58 @@ from .config import SRPConfig
|
|
|
27
27
|
from .heuristics import count_loc, count_methods, has_responsibility_keyword
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def find_all_classes(tree: ast.AST) -> list[ast.ClassDef]:
|
|
31
|
+
"""Find all class definitions in AST.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
tree: Root AST node to search
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of all class definition nodes
|
|
38
|
+
"""
|
|
39
|
+
classes = []
|
|
40
|
+
for node in ast.walk(tree):
|
|
41
|
+
if isinstance(node, ast.ClassDef):
|
|
42
|
+
classes.append(node)
|
|
43
|
+
return classes
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def analyze_class(class_node: ast.ClassDef, source: str, config: SRPConfig) -> dict[str, Any]:
|
|
47
|
+
"""Analyze a class for SRP metrics.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
class_node: AST node representing a class definition
|
|
51
|
+
source: Full source code of the file
|
|
52
|
+
config: SRP configuration with thresholds and keywords
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dictionary with class metrics (name, method_count, loc, etc.)
|
|
56
|
+
"""
|
|
57
|
+
method_count = count_methods(class_node)
|
|
58
|
+
loc = count_loc(class_node, source)
|
|
59
|
+
has_keyword = has_responsibility_keyword(class_node.name, config.keywords)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
"class_name": class_node.name,
|
|
63
|
+
"method_count": method_count,
|
|
64
|
+
"loc": loc,
|
|
65
|
+
"has_keyword": has_keyword,
|
|
66
|
+
"line": class_node.lineno,
|
|
67
|
+
"column": class_node.col_offset,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Legacy class wrapper for backward compatibility
|
|
30
72
|
class PythonSRPAnalyzer:
|
|
31
|
-
"""Analyzes Python classes for SRP violations.
|
|
73
|
+
"""Analyzes Python classes for SRP violations.
|
|
74
|
+
|
|
75
|
+
Note: This class is a thin wrapper around module-level functions
|
|
76
|
+
for backward compatibility.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
"""Initialize the analyzer."""
|
|
81
|
+
pass # No state needed
|
|
32
82
|
|
|
33
83
|
def find_all_classes(self, tree: ast.AST) -> list[ast.ClassDef]:
|
|
34
84
|
"""Find all class definitions in AST.
|
|
@@ -39,11 +89,7 @@ class PythonSRPAnalyzer:
|
|
|
39
89
|
Returns:
|
|
40
90
|
List of all class definition nodes
|
|
41
91
|
"""
|
|
42
|
-
|
|
43
|
-
for node in ast.walk(tree):
|
|
44
|
-
if isinstance(node, ast.ClassDef):
|
|
45
|
-
classes.append(node)
|
|
46
|
-
return classes
|
|
92
|
+
return find_all_classes(tree)
|
|
47
93
|
|
|
48
94
|
def analyze_class(
|
|
49
95
|
self, class_node: ast.ClassDef, source: str, config: SRPConfig
|
|
@@ -58,15 +104,4 @@ class PythonSRPAnalyzer:
|
|
|
58
104
|
Returns:
|
|
59
105
|
Dictionary with class metrics (name, method_count, loc, etc.)
|
|
60
106
|
"""
|
|
61
|
-
|
|
62
|
-
loc = count_loc(class_node, source)
|
|
63
|
-
has_keyword = has_responsibility_keyword(class_node.name, config.keywords)
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
"class_name": class_node.name,
|
|
67
|
-
"method_count": method_count,
|
|
68
|
-
"loc": loc,
|
|
69
|
-
"has_keyword": has_keyword,
|
|
70
|
-
"line": class_node.lineno,
|
|
71
|
-
"column": class_node.col_offset,
|
|
72
|
-
}
|
|
107
|
+
return analyze_class(class_node, source, config)
|