thailint 0.12.0__py3-none-any.whl → 0.14.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/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +3 -0
- src/cli/config.py +12 -12
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +9 -0
- src/cli/linters/code_patterns.py +107 -257
- src/cli/linters/code_smells.py +48 -165
- src/cli/linters/documentation.py +21 -95
- src/cli/linters/performance.py +274 -0
- src/cli/linters/shared.py +232 -6
- src/cli/linters/structure.py +26 -21
- src/cli/linters/structure_quality.py +28 -21
- src/cli_main.py +3 -0
- src/config.py +2 -1
- src/core/base.py +3 -2
- src/core/cli_utils.py +3 -1
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/rule_discovery.py +5 -1
- src/core/violation_builder.py +3 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +225 -383
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -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 +12 -0
- src/linters/collection_pipeline/continue_analyzer.py +2 -8
- src/linters/collection_pipeline/detector.py +262 -32
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +18 -35
- src/linters/collection_pipeline/suggestion_builder.py +68 -1
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +7 -4
- src/linters/dry/cache.py +7 -2
- src/linters/dry/config.py +7 -1
- src/linters/dry/constant_matcher.py +34 -25
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +48 -25
- src/linters/dry/python_analyzer.py +18 -10
- src/linters/dry/python_constant_extractor.py +51 -52
- src/linters/dry/single_statement_detector.py +14 -12
- src/linters/dry/token_hasher.py +115 -115
- src/linters/dry/typescript_analyzer.py +11 -6
- src/linters/dry/typescript_constant_extractor.py +4 -0
- src/linters/dry/typescript_statement_detector.py +208 -208
- src/linters/dry/typescript_value_extractor.py +3 -0
- src/linters/dry/violation_filter.py +1 -4
- src/linters/dry/violation_generator.py +1 -4
- src/linters/file_header/atemporal_detector.py +58 -40
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/config.py +14 -0
- src/linters/file_header/field_validator.py +5 -8
- src/linters/file_header/linter.py +19 -12
- src/linters/file_header/markdown_parser.py +6 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/linter.py +22 -8
- src/linters/file_placement/pattern_matcher.py +21 -4
- src/linters/file_placement/pattern_validator.py +21 -7
- src/linters/file_placement/rule_checker.py +2 -2
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +66 -0
- src/linters/lazy_ignores/directive_utils.py +121 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +135 -0
- src/linters/lazy_ignores/python_analyzer.py +205 -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 +69 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +131 -0
- src/linters/lbyl/__init__.py +29 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/pattern_detectors/__init__.py +25 -0
- src/linters/lbyl/pattern_detectors/base.py +46 -0
- src/linters/magic_numbers/context_analyzer.py +227 -229
- src/linters/magic_numbers/linter.py +20 -15
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -16
- src/linters/method_property/config.py +4 -1
- src/linters/method_property/linter.py +5 -10
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/linter.py +11 -6
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- 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/linter.py +6 -4
- src/linters/print_statements/python_analyzer.py +85 -81
- src/linters/print_statements/typescript_analyzer.py +6 -15
- src/linters/srp/heuristics.py +4 -4
- src/linters/srp/linter.py +12 -12
- src/linters/srp/violation_builder.py +0 -4
- src/linters/stateless_class/linter.py +30 -36
- src/linters/stateless_class/python_analyzer.py +11 -20
- src/linters/stringly_typed/config.py +4 -5
- src/linters/stringly_typed/context_filter.py +410 -410
- src/linters/stringly_typed/function_call_violation_builder.py +93 -95
- src/linters/stringly_typed/linter.py +48 -16
- src/linters/stringly_typed/python/analyzer.py +5 -1
- src/linters/stringly_typed/python/call_tracker.py +8 -5
- src/linters/stringly_typed/python/comparison_tracker.py +10 -5
- src/linters/stringly_typed/python/condition_extractor.py +3 -0
- src/linters/stringly_typed/python/conditional_detector.py +4 -1
- src/linters/stringly_typed/python/match_analyzer.py +8 -2
- src/linters/stringly_typed/python/validation_detector.py +3 -0
- src/linters/stringly_typed/storage.py +14 -14
- src/linters/stringly_typed/typescript/call_tracker.py +9 -3
- src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
- src/linters/stringly_typed/violation_generator.py +288 -259
- src/orchestrator/core.py +13 -4
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.14.0.dist-info/METADATA +185 -0
- thailint-0.14.0.dist-info/RECORD +199 -0
- thailint-0.12.0.dist-info/METADATA +0 -1667
- thailint-0.12.0.dist-info/RECORD +0 -164
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,9 +10,9 @@ Overview: Provides PythonPrintStatementAnalyzer class that traverses Python AST
|
|
|
10
10
|
Returns structured data about each print call including the AST node, parent context,
|
|
11
11
|
and line number for violation reporting. Handles both simple print() and builtins.print() calls.
|
|
12
12
|
|
|
13
|
-
Dependencies: ast module for AST parsing and node types
|
|
13
|
+
Dependencies: ast module for AST parsing and node types, analyzers.ast_utils
|
|
14
14
|
|
|
15
|
-
Exports: PythonPrintStatementAnalyzer class
|
|
15
|
+
Exports: PythonPrintStatementAnalyzer class, is_print_call function, is_main_if_block function
|
|
16
16
|
|
|
17
17
|
Interfaces: find_print_calls(tree) -> list[tuple[Call, AST | None, int]], is_in_main_block(node) -> bool
|
|
18
18
|
|
|
@@ -21,8 +21,87 @@ Implementation: AST walk pattern with parent map for context detection and __mai
|
|
|
21
21
|
|
|
22
22
|
import ast
|
|
23
23
|
|
|
24
|
+
from src.analyzers.ast_utils import build_parent_map
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
# --- Pure helper functions for print call detection ---
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_print_call(node: ast.Call) -> bool:
|
|
30
|
+
"""Check if a Call node is calling print().
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
node: The Call node to check
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if this is a print() call
|
|
37
|
+
"""
|
|
38
|
+
return _is_simple_print(node) or _is_builtins_print(node)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_simple_print(node: ast.Call) -> bool:
|
|
42
|
+
"""Check for simple print() call."""
|
|
43
|
+
return isinstance(node.func, ast.Name) and node.func.id == "print"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _is_builtins_print(node: ast.Call) -> bool:
|
|
47
|
+
"""Check for builtins.print() call."""
|
|
48
|
+
if not isinstance(node.func, ast.Attribute):
|
|
49
|
+
return False
|
|
50
|
+
if node.func.attr != "print":
|
|
51
|
+
return False
|
|
52
|
+
return isinstance(node.func.value, ast.Name) and node.func.value.id == "builtins"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# --- Pure helper functions for __main__ block detection ---
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_main_if_block(node: ast.AST) -> bool:
|
|
59
|
+
"""Check if node is an `if __name__ == "__main__":` statement.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
node: AST node to check
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if this is a __main__ if block
|
|
66
|
+
"""
|
|
67
|
+
if not isinstance(node, ast.If):
|
|
68
|
+
return False
|
|
69
|
+
if not isinstance(node.test, ast.Compare):
|
|
70
|
+
return False
|
|
71
|
+
return _is_main_comparison(node.test)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_main_comparison(test: ast.Compare) -> bool:
|
|
75
|
+
"""Check if comparison is __name__ == '__main__'."""
|
|
76
|
+
if not _is_name_identifier(test.left):
|
|
77
|
+
return False
|
|
78
|
+
if not _has_single_eq_operator(test):
|
|
79
|
+
return False
|
|
80
|
+
return _compares_to_main(test)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_name_identifier(node: ast.expr) -> bool:
|
|
84
|
+
"""Check if node is the __name__ identifier."""
|
|
85
|
+
return isinstance(node, ast.Name) and node.id == "__name__"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _has_single_eq_operator(test: ast.Compare) -> bool:
|
|
89
|
+
"""Check if comparison has single == operator."""
|
|
90
|
+
return len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _compares_to_main(test: ast.Compare) -> bool:
|
|
94
|
+
"""Check if comparison is to '__main__' string."""
|
|
95
|
+
if len(test.comparators) != 1:
|
|
96
|
+
return False
|
|
97
|
+
comparator = test.comparators[0]
|
|
98
|
+
return isinstance(comparator, ast.Constant) and comparator.value == "__main__"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --- Analyzer class with stateful parent tracking ---
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PythonPrintStatementAnalyzer:
|
|
26
105
|
"""Analyzes Python AST to find print() calls."""
|
|
27
106
|
|
|
28
107
|
def __init__(self) -> None:
|
|
@@ -40,24 +119,10 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
|
|
|
40
119
|
List of tuples (node, parent, line_number)
|
|
41
120
|
"""
|
|
42
121
|
self.print_calls = []
|
|
43
|
-
self.parent_map =
|
|
44
|
-
self._build_parent_map(tree)
|
|
122
|
+
self.parent_map = build_parent_map(tree)
|
|
45
123
|
self._collect_print_calls(tree)
|
|
46
124
|
return self.print_calls
|
|
47
125
|
|
|
48
|
-
def _build_parent_map(self, node: ast.AST, parent: ast.AST | None = None) -> None:
|
|
49
|
-
"""Build a map of nodes to their parents.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
node: Current AST node
|
|
53
|
-
parent: Parent of current node
|
|
54
|
-
"""
|
|
55
|
-
if parent is not None:
|
|
56
|
-
self.parent_map[node] = parent
|
|
57
|
-
|
|
58
|
-
for child in ast.iter_child_nodes(node):
|
|
59
|
-
self._build_parent_map(child, node)
|
|
60
|
-
|
|
61
126
|
def _collect_print_calls(self, tree: ast.AST) -> None:
|
|
62
127
|
"""Walk tree and collect all print() calls.
|
|
63
128
|
|
|
@@ -65,34 +130,11 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
|
|
|
65
130
|
tree: AST to traverse
|
|
66
131
|
"""
|
|
67
132
|
for node in ast.walk(tree):
|
|
68
|
-
if isinstance(node, ast.Call) and
|
|
133
|
+
if isinstance(node, ast.Call) and is_print_call(node):
|
|
69
134
|
parent = self.parent_map.get(node)
|
|
70
135
|
line_number = node.lineno if hasattr(node, "lineno") else 0
|
|
71
136
|
self.print_calls.append((node, parent, line_number))
|
|
72
137
|
|
|
73
|
-
def _is_print_call(self, node: ast.Call) -> bool:
|
|
74
|
-
"""Check if a Call node is calling print().
|
|
75
|
-
|
|
76
|
-
Args:
|
|
77
|
-
node: The Call node to check
|
|
78
|
-
|
|
79
|
-
Returns:
|
|
80
|
-
True if this is a print() call
|
|
81
|
-
"""
|
|
82
|
-
return self._is_simple_print(node) or self._is_builtins_print(node)
|
|
83
|
-
|
|
84
|
-
def _is_simple_print(self, node: ast.Call) -> bool:
|
|
85
|
-
"""Check for simple print() call."""
|
|
86
|
-
return isinstance(node.func, ast.Name) and node.func.id == "print"
|
|
87
|
-
|
|
88
|
-
def _is_builtins_print(self, node: ast.Call) -> bool:
|
|
89
|
-
"""Check for builtins.print() call."""
|
|
90
|
-
if not isinstance(node.func, ast.Attribute):
|
|
91
|
-
return False
|
|
92
|
-
if node.func.attr != "print":
|
|
93
|
-
return False
|
|
94
|
-
return isinstance(node.func.value, ast.Name) and node.func.value.id == "builtins"
|
|
95
|
-
|
|
96
138
|
def is_in_main_block(self, node: ast.AST) -> bool:
|
|
97
139
|
"""Check if node is within `if __name__ == "__main__":` block.
|
|
98
140
|
|
|
@@ -105,45 +147,7 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
|
|
|
105
147
|
current = node
|
|
106
148
|
while current in self.parent_map:
|
|
107
149
|
parent = self.parent_map[current]
|
|
108
|
-
if
|
|
150
|
+
if is_main_if_block(parent):
|
|
109
151
|
return True
|
|
110
152
|
current = parent
|
|
111
153
|
return False
|
|
112
|
-
|
|
113
|
-
def _is_main_if_block(self, node: ast.AST) -> bool:
|
|
114
|
-
"""Check if node is an `if __name__ == "__main__":` statement.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
node: AST node to check
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
True if this is a __main__ if block
|
|
121
|
-
"""
|
|
122
|
-
if not isinstance(node, ast.If):
|
|
123
|
-
return False
|
|
124
|
-
if not isinstance(node.test, ast.Compare):
|
|
125
|
-
return False
|
|
126
|
-
return self._is_main_comparison(node.test)
|
|
127
|
-
|
|
128
|
-
def _is_main_comparison(self, test: ast.Compare) -> bool:
|
|
129
|
-
"""Check if comparison is __name__ == '__main__'."""
|
|
130
|
-
if not self._is_name_identifier(test.left):
|
|
131
|
-
return False
|
|
132
|
-
if not self._has_single_eq_operator(test):
|
|
133
|
-
return False
|
|
134
|
-
return self._compares_to_main(test)
|
|
135
|
-
|
|
136
|
-
def _is_name_identifier(self, node: ast.expr) -> bool:
|
|
137
|
-
"""Check if node is the __name__ identifier."""
|
|
138
|
-
return isinstance(node, ast.Name) and node.id == "__name__"
|
|
139
|
-
|
|
140
|
-
def _has_single_eq_operator(self, test: ast.Compare) -> bool:
|
|
141
|
-
"""Check if comparison has single == operator."""
|
|
142
|
-
return len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)
|
|
143
|
-
|
|
144
|
-
def _compares_to_main(self, test: ast.Compare) -> bool:
|
|
145
|
-
"""Check if comparison is to '__main__' string."""
|
|
146
|
-
if len(test.comparators) != 1:
|
|
147
|
-
return False
|
|
148
|
-
comparator = test.comparators[0]
|
|
149
|
-
return isinstance(comparator, ast.Constant) and comparator.value == "__main__"
|
|
@@ -18,32 +18,23 @@ Exports: TypeScriptPrintStatementAnalyzer class
|
|
|
18
18
|
Interfaces: find_console_calls(root_node, methods) -> list[tuple[Node, str, int]]
|
|
19
19
|
|
|
20
20
|
Implementation: Tree-sitter node traversal with call_expression and member_expression pattern matching
|
|
21
|
+
|
|
21
22
|
"""
|
|
22
23
|
|
|
23
24
|
import logging
|
|
24
|
-
from typing import Any
|
|
25
25
|
|
|
26
|
-
from src.analyzers.typescript_base import
|
|
26
|
+
from src.analyzers.typescript_base import (
|
|
27
|
+
TREE_SITTER_AVAILABLE,
|
|
28
|
+
Node,
|
|
29
|
+
TypeScriptBaseAnalyzer,
|
|
30
|
+
)
|
|
27
31
|
|
|
28
32
|
logger = logging.getLogger(__name__)
|
|
29
33
|
|
|
30
|
-
# dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
|
|
31
|
-
try:
|
|
32
|
-
from tree_sitter import Node
|
|
33
|
-
|
|
34
|
-
TREE_SITTER_AVAILABLE = True
|
|
35
|
-
except ImportError:
|
|
36
|
-
TREE_SITTER_AVAILABLE = False
|
|
37
|
-
Node = Any # type: ignore
|
|
38
|
-
|
|
39
34
|
|
|
40
35
|
class TypeScriptPrintStatementAnalyzer(TypeScriptBaseAnalyzer):
|
|
41
36
|
"""Analyzes TypeScript/JavaScript code for console.* calls using Tree-sitter."""
|
|
42
37
|
|
|
43
|
-
def __init__(self) -> None: # pylint: disable=useless-parent-delegation
|
|
44
|
-
"""Initialize the TypeScript print statement analyzer."""
|
|
45
|
-
super().__init__() # Sets self.tree_sitter_available from base class
|
|
46
|
-
|
|
47
38
|
def find_console_calls(self, root_node: Node, methods: set[str]) -> list[tuple[Node, str, int]]:
|
|
48
39
|
"""Find all console.* calls matching the specified methods.
|
|
49
40
|
|
src/linters/srp/heuristics.py
CHANGED
|
@@ -84,7 +84,7 @@ def has_property_decorator(func_node: ast.FunctionDef | ast.AsyncFunctionDef) ->
|
|
|
84
84
|
Returns:
|
|
85
85
|
True if function has @property decorator
|
|
86
86
|
"""
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
return any(
|
|
88
|
+
isinstance(decorator, ast.Name) and decorator.id == "property"
|
|
89
|
+
for decorator in func_node.decorator_list
|
|
90
|
+
)
|
src/linters/srp/linter.py
CHANGED
|
@@ -16,9 +16,13 @@ Exports: SRPRule class
|
|
|
16
16
|
Interfaces: SRPRule.check(context) -> list[Violation], properties for rule metadata
|
|
17
17
|
|
|
18
18
|
Implementation: Composition pattern with helper classes, heuristic-based SRP analysis
|
|
19
|
+
|
|
20
|
+
Suppressions:
|
|
21
|
+
- type:ignore[return-value]: Generic TypeScript analyzer return type variance
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
24
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
25
|
+
from src.core.constants import Language
|
|
22
26
|
from src.core.linter_utils import load_linter_config
|
|
23
27
|
from src.core.types import Violation
|
|
24
28
|
from src.linter_config.ignore import get_ignore_parser
|
|
@@ -100,10 +104,10 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
100
104
|
Returns:
|
|
101
105
|
List of violations found
|
|
102
106
|
"""
|
|
103
|
-
if context.language ==
|
|
107
|
+
if context.language == Language.PYTHON:
|
|
104
108
|
return self._check_python(context, config)
|
|
105
109
|
|
|
106
|
-
if context.language in (
|
|
110
|
+
if context.language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
|
|
107
111
|
return self._check_typescript(context, config)
|
|
108
112
|
|
|
109
113
|
return []
|
|
@@ -133,10 +137,7 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
133
137
|
return False
|
|
134
138
|
|
|
135
139
|
file_path = str(context.file_path)
|
|
136
|
-
for pattern in config.ignore
|
|
137
|
-
if pattern in file_path:
|
|
138
|
-
return True
|
|
139
|
-
return False
|
|
140
|
+
return any(pattern in file_path for pattern in config.ignore)
|
|
140
141
|
|
|
141
142
|
def _check_python(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
142
143
|
"""Check Python code for SRP violations.
|
|
@@ -170,13 +171,12 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
170
171
|
Returns:
|
|
171
172
|
List of violations
|
|
172
173
|
"""
|
|
173
|
-
violations = []
|
|
174
174
|
valid_metrics = (m for m in metrics_list if isinstance(m, dict))
|
|
175
|
-
|
|
176
|
-
violation
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
175
|
+
return [
|
|
176
|
+
violation
|
|
177
|
+
for metrics in valid_metrics
|
|
178
|
+
if (violation := self._create_violation_if_needed(metrics, config, context))
|
|
179
|
+
]
|
|
180
180
|
|
|
181
181
|
def _create_violation_if_needed(
|
|
182
182
|
self,
|
|
@@ -29,10 +29,6 @@ from src.core.violation_builder import BaseViolationBuilder, ViolationInfo
|
|
|
29
29
|
class ViolationBuilder(BaseViolationBuilder):
|
|
30
30
|
"""Builds SRP violations with messages and suggestions."""
|
|
31
31
|
|
|
32
|
-
def __init__(self) -> None: # pylint: disable=useless-parent-delegation
|
|
33
|
-
"""Initialize the violation builder."""
|
|
34
|
-
super().__init__() # Inherits from BaseViolationBuilder
|
|
35
|
-
|
|
36
32
|
def build_violation(
|
|
37
33
|
self,
|
|
38
34
|
metrics: dict[str, Any],
|
|
@@ -20,13 +20,20 @@ Interfaces: StatelessClassRule.check(context) -> list[Violation]
|
|
|
20
20
|
|
|
21
21
|
Implementation: Composition pattern delegating analysis to specialized analyzer with
|
|
22
22
|
config loading and comprehensive ignore checking
|
|
23
|
+
|
|
24
|
+
Suppressions:
|
|
25
|
+
- B101: Type narrowing assertion after _should_analyze guard (can't fail)
|
|
26
|
+
- srp,dry: Rule class coordinates analyzer, config, and ignore checking. Method count
|
|
27
|
+
exceeds limit due to comprehensive 5-level ignore system support.
|
|
23
28
|
"""
|
|
24
29
|
|
|
25
30
|
from pathlib import Path
|
|
26
31
|
|
|
27
32
|
from src.core.base import BaseLintContext, BaseLintRule
|
|
33
|
+
from src.core.constants import HEADER_SCAN_LINES, IgnoreDirective, Language
|
|
28
34
|
from src.core.types import Severity, Violation
|
|
29
35
|
from src.linter_config.ignore import get_ignore_parser
|
|
36
|
+
from src.linter_config.rule_matcher import rule_matches
|
|
30
37
|
|
|
31
38
|
from .config import StatelessClassConfig
|
|
32
39
|
from .python_analyzer import ClassInfo, StatelessClassAnalyzer
|
|
@@ -67,20 +74,29 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
67
74
|
return []
|
|
68
75
|
|
|
69
76
|
config = self._load_config(context)
|
|
70
|
-
if not config.enabled:
|
|
71
|
-
return []
|
|
72
|
-
|
|
73
|
-
if self._is_file_ignored(context, config):
|
|
77
|
+
if not config.enabled or self._should_skip_file(context, config):
|
|
74
78
|
return []
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
# _should_analyze ensures file_content is set
|
|
81
|
+
assert context.file_content is not None # nosec B101
|
|
78
82
|
|
|
79
83
|
analyzer = StatelessClassAnalyzer(min_methods=config.min_methods)
|
|
80
|
-
stateless_classes = analyzer.analyze(context.file_content)
|
|
84
|
+
stateless_classes = analyzer.analyze(context.file_content)
|
|
81
85
|
|
|
82
86
|
return self._filter_ignored_violations(stateless_classes, context)
|
|
83
87
|
|
|
88
|
+
def _should_skip_file(self, context: BaseLintContext, config: StatelessClassConfig) -> bool:
|
|
89
|
+
"""Check if file should be skipped due to ignore patterns or directives.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
context: Lint context
|
|
93
|
+
config: Configuration
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if file should be skipped
|
|
97
|
+
"""
|
|
98
|
+
return self._is_file_ignored(context, config) or self._has_file_level_ignore(context)
|
|
99
|
+
|
|
84
100
|
def _should_analyze(self, context: BaseLintContext) -> bool:
|
|
85
101
|
"""Check if context should be analyzed.
|
|
86
102
|
|
|
@@ -90,7 +106,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
90
106
|
Returns:
|
|
91
107
|
True if should analyze
|
|
92
108
|
"""
|
|
93
|
-
return context.language ==
|
|
109
|
+
return context.language == Language.PYTHON and context.file_content is not None
|
|
94
110
|
|
|
95
111
|
def _load_config(self, context: BaseLintContext) -> StatelessClassConfig:
|
|
96
112
|
"""Load configuration from context.
|
|
@@ -129,10 +145,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
129
145
|
return False
|
|
130
146
|
|
|
131
147
|
file_path = Path(context.file_path)
|
|
132
|
-
for pattern in config.ignore
|
|
133
|
-
if self._matches_pattern(file_path, pattern):
|
|
134
|
-
return True
|
|
135
|
-
return False
|
|
148
|
+
return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
|
|
136
149
|
|
|
137
150
|
def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
|
|
138
151
|
"""Check if file path matches a glob pattern.
|
|
@@ -162,12 +175,9 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
162
175
|
if not context.file_content:
|
|
163
176
|
return False
|
|
164
177
|
|
|
165
|
-
# Check first
|
|
166
|
-
lines = context.file_content.splitlines()[:
|
|
167
|
-
for line in lines
|
|
168
|
-
if self._is_file_ignore_directive(line):
|
|
169
|
-
return True
|
|
170
|
-
return False
|
|
178
|
+
# Check first lines for ignore-file directive
|
|
179
|
+
lines = context.file_content.splitlines()[:HEADER_SCAN_LINES]
|
|
180
|
+
return any(self._is_file_ignore_directive(line) for line in lines)
|
|
171
181
|
|
|
172
182
|
def _is_file_ignore_directive(self, line: str) -> bool:
|
|
173
183
|
"""Check if line is a file-level ignore directive.
|
|
@@ -218,23 +228,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
218
228
|
Returns:
|
|
219
229
|
True if pattern matches this rule
|
|
220
230
|
"""
|
|
221
|
-
|
|
222
|
-
pattern_lower = rule_pattern.lower()
|
|
223
|
-
|
|
224
|
-
# Exact match
|
|
225
|
-
if rule_id_lower == pattern_lower:
|
|
226
|
-
return True
|
|
227
|
-
|
|
228
|
-
# Prefix match: stateless-class matches stateless-class.violation
|
|
229
|
-
if rule_id_lower.startswith(pattern_lower + "."):
|
|
230
|
-
return True
|
|
231
|
-
|
|
232
|
-
# Wildcard match: stateless-class.* matches stateless-class.violation
|
|
233
|
-
if pattern_lower.endswith("*"):
|
|
234
|
-
prefix = pattern_lower[:-1]
|
|
235
|
-
return rule_id_lower.startswith(prefix)
|
|
236
|
-
|
|
237
|
-
return False
|
|
231
|
+
return rule_matches(self.rule_id, rule_pattern)
|
|
238
232
|
|
|
239
233
|
def _filter_ignored_violations(
|
|
240
234
|
self, classes: list[ClassInfo], context: BaseLintContext
|
|
@@ -330,7 +324,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
330
324
|
return True
|
|
331
325
|
|
|
332
326
|
# Rule-specific ignore
|
|
333
|
-
return self._matches_rule_ignore(line,
|
|
327
|
+
return self._matches_rule_ignore(line, IgnoreDirective.IGNORE)
|
|
334
328
|
|
|
335
329
|
def _create_violation(self, info: ClassInfo, context: BaseLintContext) -> Violation:
|
|
336
330
|
"""Create violation from class info.
|
|
@@ -145,10 +145,10 @@ def _has_constructor(class_node: ast.ClassDef) -> bool:
|
|
|
145
145
|
True if class has constructor
|
|
146
146
|
"""
|
|
147
147
|
constructor_names = ("__init__", "__new__")
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
return any(
|
|
149
|
+
isinstance(item, ast.FunctionDef) and item.name in constructor_names
|
|
150
|
+
for item in class_node.body
|
|
151
|
+
)
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
def _is_exception_case(class_node: ast.ClassDef) -> bool:
|
|
@@ -174,10 +174,7 @@ def _inherits_from_abc_or_protocol(class_node: ast.ClassDef) -> bool:
|
|
|
174
174
|
Returns:
|
|
175
175
|
True if inherits from ABC or Protocol
|
|
176
176
|
"""
|
|
177
|
-
for base in class_node.bases
|
|
178
|
-
if _get_base_name(base) in ("ABC", "Protocol"):
|
|
179
|
-
return True
|
|
180
|
-
return False
|
|
177
|
+
return any(_get_base_name(base) in ("ABC", "Protocol") for base in class_node.bases)
|
|
181
178
|
|
|
182
179
|
|
|
183
180
|
def _get_base_name(base: ast.expr) -> str:
|
|
@@ -205,10 +202,7 @@ def _has_class_attributes(class_node: ast.ClassDef) -> bool:
|
|
|
205
202
|
Returns:
|
|
206
203
|
True if class has class attributes
|
|
207
204
|
"""
|
|
208
|
-
for item in class_node.body
|
|
209
|
-
if isinstance(item, (ast.Assign, ast.AnnAssign)):
|
|
210
|
-
return True
|
|
211
|
-
return False
|
|
205
|
+
return any(isinstance(item, (ast.Assign, ast.AnnAssign)) for item in class_node.body)
|
|
212
206
|
|
|
213
207
|
|
|
214
208
|
def _has_instance_attributes(class_node: ast.ClassDef) -> bool:
|
|
@@ -220,10 +214,10 @@ def _has_instance_attributes(class_node: ast.ClassDef) -> bool:
|
|
|
220
214
|
Returns:
|
|
221
215
|
True if any method assigns to self
|
|
222
216
|
"""
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
217
|
+
return any(
|
|
218
|
+
isinstance(item, ast.FunctionDef) and _method_has_self_assignment(item)
|
|
219
|
+
for item in class_node.body
|
|
220
|
+
)
|
|
227
221
|
|
|
228
222
|
|
|
229
223
|
def _method_has_self_assignment(method: ast.FunctionDef) -> bool:
|
|
@@ -235,10 +229,7 @@ def _method_has_self_assignment(method: ast.FunctionDef) -> bool:
|
|
|
235
229
|
Returns:
|
|
236
230
|
True if method assigns to self
|
|
237
231
|
"""
|
|
238
|
-
for node in ast.walk(method)
|
|
239
|
-
if _is_self_attribute_assignment(node):
|
|
240
|
-
return True
|
|
241
|
-
return False
|
|
232
|
+
return any(_is_self_attribute_assignment(node) for node in ast.walk(method))
|
|
242
233
|
|
|
243
234
|
|
|
244
235
|
def _is_self_attribute_assignment(node: ast.AST) -> bool:
|
|
@@ -18,11 +18,10 @@ Exports: StringlyTypedConfig dataclass, default constants
|
|
|
18
18
|
Interfaces: StringlyTypedConfig.from_dict() class method for configuration loading
|
|
19
19
|
|
|
20
20
|
Implementation: Dataclass with sensible defaults, validation in __post_init__, and config
|
|
21
|
-
loading from dictionary with language-specific override support
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
the established pattern in DRYConfig which has the same suppression.
|
|
21
|
+
loading from dictionary with language-specific override support
|
|
22
|
+
|
|
23
|
+
Suppressions:
|
|
24
|
+
- too-many-instance-attributes: Configuration dataclass with cohesive detection settings
|
|
26
25
|
"""
|
|
27
26
|
|
|
28
27
|
from dataclasses import dataclass, field
|