thailint 0.15.8__py3-none-any.whl → 0.17.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/cli/config.py +4 -12
- src/cli/linters/__init__.py +13 -3
- src/cli/linters/code_patterns.py +42 -38
- src/cli/linters/code_smells.py +8 -17
- src/cli/linters/documentation.py +3 -6
- src/cli/linters/performance.py +4 -10
- src/cli/linters/rust.py +177 -0
- src/cli/linters/shared.py +2 -7
- src/cli/linters/structure.py +4 -11
- src/cli/linters/structure_quality.py +4 -11
- src/cli/main.py +9 -12
- src/cli/utils.py +7 -16
- src/core/__init__.py +14 -0
- src/core/base.py +30 -0
- src/core/constants.py +1 -0
- src/core/linter_utils.py +42 -1
- src/core/rule_aliases.py +84 -0
- src/linter_config/rule_matcher.py +53 -8
- src/linters/blocking_async/__init__.py +31 -0
- src/linters/blocking_async/config.py +67 -0
- src/linters/blocking_async/linter.py +183 -0
- src/linters/blocking_async/rust_analyzer.py +419 -0
- src/linters/blocking_async/violation_builder.py +97 -0
- src/linters/clone_abuse/__init__.py +31 -0
- src/linters/clone_abuse/config.py +65 -0
- src/linters/clone_abuse/linter.py +183 -0
- src/linters/clone_abuse/rust_analyzer.py +356 -0
- src/linters/clone_abuse/violation_builder.py +94 -0
- src/linters/magic_numbers/linter.py +92 -0
- src/linters/magic_numbers/rust_analyzer.py +148 -0
- src/linters/magic_numbers/violation_builder.py +31 -0
- src/linters/nesting/linter.py +50 -0
- src/linters/nesting/rust_analyzer.py +118 -0
- src/linters/nesting/violation_builder.py +32 -0
- src/linters/print_statements/__init__.py +23 -11
- src/linters/print_statements/conditional_verbose_analyzer.py +200 -0
- src/linters/print_statements/conditional_verbose_rule.py +254 -0
- src/linters/print_statements/linter.py +2 -2
- src/linters/srp/class_analyzer.py +49 -0
- src/linters/srp/linter.py +22 -0
- src/linters/srp/rust_analyzer.py +206 -0
- src/linters/unwrap_abuse/__init__.py +30 -0
- src/linters/unwrap_abuse/config.py +59 -0
- src/linters/unwrap_abuse/linter.py +166 -0
- src/linters/unwrap_abuse/rust_analyzer.py +118 -0
- src/linters/unwrap_abuse/violation_builder.py +89 -0
- src/templates/thailint_config_template.yaml +88 -0
- {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/METADATA +7 -3
- {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/RECORD +52 -30
- {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/WHEEL +0 -0
- {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Python AST analysis for finding conditional verbose logging patterns
|
|
3
|
+
|
|
4
|
+
Scope: Detection of if verbose: logger.*() anti-patterns in Python code
|
|
5
|
+
|
|
6
|
+
Overview: Provides ConditionalVerboseAnalyzer class that traverses Python AST to find logging calls
|
|
7
|
+
that are conditionally guarded by verbose flags. Detects patterns like 'if verbose: logger.debug()'
|
|
8
|
+
or 'if self.verbose: logger.info()' which are anti-patterns because logging levels should be
|
|
9
|
+
configured at the logger level rather than through code conditionals. Supports detection of
|
|
10
|
+
various verbose condition patterns including simple names, attribute access, dict access, and
|
|
11
|
+
method calls on context objects.
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module for AST parsing and node types
|
|
14
|
+
|
|
15
|
+
Exports: ConditionalVerboseAnalyzer class, is_verbose_condition function, is_logger_call function
|
|
16
|
+
|
|
17
|
+
Interfaces: find_conditional_verbose_calls(tree) -> list[tuple[If, Call, int]]
|
|
18
|
+
|
|
19
|
+
Implementation: AST walk pattern with condition matching for verbose patterns and logger call detection
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
|
|
24
|
+
# Logger methods that indicate a logging call
|
|
25
|
+
LOGGER_METHODS = frozenset({"debug", "info", "warning", "error", "critical", "log", "exception"})
|
|
26
|
+
|
|
27
|
+
# Verbose-related names that typically guard logging
|
|
28
|
+
VERBOSE_NAMES = frozenset({"verbose", "debug", "is_verbose", "is_debug"})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_verbose_condition(test: ast.expr) -> bool:
|
|
32
|
+
"""Check if an expression is a verbose-like condition.
|
|
33
|
+
|
|
34
|
+
Matches patterns like:
|
|
35
|
+
- verbose
|
|
36
|
+
- self.verbose
|
|
37
|
+
- config.verbose
|
|
38
|
+
- params.verbose
|
|
39
|
+
- ctx.obj.get("verbose")
|
|
40
|
+
- ctx.obj["verbose"]
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
test: The condition expression to check
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True if the condition appears to be a verbose check
|
|
47
|
+
"""
|
|
48
|
+
return (
|
|
49
|
+
_is_simple_verbose_name(test)
|
|
50
|
+
or _is_verbose_attribute(test)
|
|
51
|
+
or _is_verbose_subscript(test)
|
|
52
|
+
or _is_verbose_dict_get(test)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_simple_verbose_name(test: ast.expr) -> bool:
|
|
57
|
+
"""Check for simple name like 'verbose' or 'debug'."""
|
|
58
|
+
return isinstance(test, ast.Name) and test.id.lower() in VERBOSE_NAMES
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_verbose_attribute(test: ast.expr) -> bool:
|
|
62
|
+
"""Check for attribute access like 'self.verbose' or 'config.verbose'."""
|
|
63
|
+
if not isinstance(test, ast.Attribute):
|
|
64
|
+
return False
|
|
65
|
+
return test.attr.lower() in VERBOSE_NAMES
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_verbose_subscript(test: ast.expr) -> bool:
|
|
69
|
+
"""Check for subscript access like 'ctx.obj["verbose"]'."""
|
|
70
|
+
if not isinstance(test, ast.Subscript):
|
|
71
|
+
return False
|
|
72
|
+
if not isinstance(test.slice, ast.Constant):
|
|
73
|
+
return False
|
|
74
|
+
value = test.slice.value
|
|
75
|
+
return isinstance(value, str) and value.lower() in VERBOSE_NAMES
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _is_verbose_dict_get(test: ast.expr) -> bool:
|
|
79
|
+
"""Check for dict.get call like 'ctx.obj.get("verbose")'."""
|
|
80
|
+
if not isinstance(test, ast.Call):
|
|
81
|
+
return False
|
|
82
|
+
if not _is_dict_get_call_with_args(test):
|
|
83
|
+
return False
|
|
84
|
+
return _first_arg_is_verbose_string(test.args[0])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _is_dict_get_call_with_args(call: ast.Call) -> bool:
|
|
88
|
+
"""Check if call is a .get() method call with arguments."""
|
|
89
|
+
if not isinstance(call.func, ast.Attribute):
|
|
90
|
+
return False
|
|
91
|
+
if call.func.attr != "get":
|
|
92
|
+
return False
|
|
93
|
+
return bool(call.args)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _first_arg_is_verbose_string(arg: ast.expr) -> bool:
|
|
97
|
+
"""Check if argument is a verbose-related string constant."""
|
|
98
|
+
if not isinstance(arg, ast.Constant):
|
|
99
|
+
return False
|
|
100
|
+
value = arg.value
|
|
101
|
+
return isinstance(value, str) and value.lower() in VERBOSE_NAMES
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def is_logger_call(node: ast.Call) -> bool:
|
|
105
|
+
"""Check if a Call node is a logger method call.
|
|
106
|
+
|
|
107
|
+
Matches patterns like:
|
|
108
|
+
- logger.debug()
|
|
109
|
+
- logging.info()
|
|
110
|
+
- self.logger.warning()
|
|
111
|
+
- log.error()
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
node: The Call node to check
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
True if this appears to be a logging call
|
|
118
|
+
"""
|
|
119
|
+
if not isinstance(node.func, ast.Attribute):
|
|
120
|
+
return False
|
|
121
|
+
return node.func.attr in LOGGER_METHODS
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _extract_logger_method(node: ast.Call) -> str:
|
|
125
|
+
"""Extract the logger method name from a call node.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
node: The Call node (must be a logger call)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The logger method name (e.g., 'debug', 'info')
|
|
132
|
+
"""
|
|
133
|
+
if isinstance(node.func, ast.Attribute):
|
|
134
|
+
return node.func.attr
|
|
135
|
+
return ""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ConditionalVerboseAnalyzer:
|
|
139
|
+
"""Analyzes Python AST to find conditional verbose logging patterns."""
|
|
140
|
+
|
|
141
|
+
def __init__(self) -> None:
|
|
142
|
+
"""Initialize the analyzer."""
|
|
143
|
+
self.violations: list[tuple[ast.If, ast.Call, int]] = []
|
|
144
|
+
|
|
145
|
+
def find_conditional_verbose_calls(
|
|
146
|
+
self, tree: ast.AST
|
|
147
|
+
) -> list[tuple[ast.If, ast.Call, str, int]]:
|
|
148
|
+
"""Find all conditional verbose logging patterns in the AST.
|
|
149
|
+
|
|
150
|
+
Looks for if statements with verbose-like conditions that contain
|
|
151
|
+
logger method calls in their body.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
tree: The AST to analyze
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of tuples (if_node, call_node, logger_method, line_number)
|
|
158
|
+
"""
|
|
159
|
+
verbose_if_nodes = (
|
|
160
|
+
node
|
|
161
|
+
for node in ast.walk(tree)
|
|
162
|
+
if isinstance(node, ast.If) and is_verbose_condition(node.test)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
results: list[tuple[ast.If, ast.Call, str, int]] = []
|
|
166
|
+
for if_node in verbose_if_nodes:
|
|
167
|
+
results.extend(self._extract_logger_call_results(if_node))
|
|
168
|
+
|
|
169
|
+
return results
|
|
170
|
+
|
|
171
|
+
def _extract_logger_call_results(
|
|
172
|
+
self, if_node: ast.If
|
|
173
|
+
) -> list[tuple[ast.If, ast.Call, str, int]]:
|
|
174
|
+
"""Extract logger call results from a verbose if node."""
|
|
175
|
+
logger_calls = self._find_logger_calls_in_body(if_node.body)
|
|
176
|
+
return [
|
|
177
|
+
(
|
|
178
|
+
if_node,
|
|
179
|
+
call_node,
|
|
180
|
+
_extract_logger_method(call_node),
|
|
181
|
+
call_node.lineno if hasattr(call_node, "lineno") else if_node.lineno,
|
|
182
|
+
)
|
|
183
|
+
for call_node in logger_calls
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
def _find_logger_calls_in_body(self, body: list[ast.stmt]) -> list[ast.Call]:
|
|
187
|
+
"""Find all logger calls in a list of statements.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
body: List of AST statements
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
List of Call nodes that are logger calls
|
|
194
|
+
"""
|
|
195
|
+
logger_calls: list[ast.Call] = []
|
|
196
|
+
for stmt in body:
|
|
197
|
+
for node in ast.walk(stmt):
|
|
198
|
+
if isinstance(node, ast.Call) and is_logger_call(node):
|
|
199
|
+
logger_calls.append(node)
|
|
200
|
+
return logger_calls
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Lint rule for detecting conditional verbose logging anti-patterns
|
|
3
|
+
|
|
4
|
+
Scope: Detection of if verbose: logger.*() patterns in Python code
|
|
5
|
+
|
|
6
|
+
Overview: Implements the ConditionalVerboseRule that detects logging calls conditionally guarded
|
|
7
|
+
by verbose flags. This is an anti-pattern because logging levels should be configured through
|
|
8
|
+
the logging framework (e.g., logger.setLevel(logging.DEBUG)) rather than through code
|
|
9
|
+
conditionals. The rule reports violations with suggestions to remove the conditional and
|
|
10
|
+
configure logging levels properly. Only applies to Python files as this pattern is specific
|
|
11
|
+
to Python logging practices.
|
|
12
|
+
|
|
13
|
+
Dependencies: BaseLintContext and BaseLintRule from core, ast module, conditional_verbose_analyzer
|
|
14
|
+
|
|
15
|
+
Exports: ConditionalVerboseRule class implementing BaseLintRule interface
|
|
16
|
+
|
|
17
|
+
Interfaces: check(context) -> list[Violation] for rule validation, standard rule properties
|
|
18
|
+
(rule_id, rule_name, description)
|
|
19
|
+
|
|
20
|
+
Implementation: AST-based analysis using ConditionalVerboseAnalyzer for pattern detection
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from src.core.base import BaseLintContext, BaseLintRule
|
|
27
|
+
from src.core.constants import Language
|
|
28
|
+
from src.core.linter_utils import has_file_content, load_linter_config
|
|
29
|
+
from src.core.types import Violation
|
|
30
|
+
from src.core.violation_utils import get_violation_line, has_python_noqa
|
|
31
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
32
|
+
|
|
33
|
+
from .conditional_verbose_analyzer import ConditionalVerboseAnalyzer
|
|
34
|
+
from .config import PrintStatementConfig
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ConditionalVerboseRule(BaseLintRule):
|
|
38
|
+
"""Detects conditional verbose logging patterns that should use log level configuration."""
|
|
39
|
+
|
|
40
|
+
def __init__(self) -> None:
|
|
41
|
+
"""Initialize the conditional verbose rule."""
|
|
42
|
+
self._ignore_parser = get_ignore_parser()
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def rule_id(self) -> str:
|
|
46
|
+
"""Unique identifier for this rule."""
|
|
47
|
+
return "improper-logging.conditional-verbose"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def rule_name(self) -> str:
|
|
51
|
+
"""Human-readable name for this rule."""
|
|
52
|
+
return "Improper Logging - Conditional Verbose"
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def description(self) -> str:
|
|
56
|
+
"""Description of what this rule checks."""
|
|
57
|
+
return "Conditional verbose logging should use log level configuration instead"
|
|
58
|
+
|
|
59
|
+
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
60
|
+
"""Check for conditional verbose logging violations.
|
|
61
|
+
|
|
62
|
+
Only applies to Python files, as this pattern is Python-specific.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
context: Lint context with file information
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List of violations found
|
|
69
|
+
"""
|
|
70
|
+
if not self._should_analyze(context):
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
tree = self._parse_python_code(context.file_content)
|
|
74
|
+
if tree is None:
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
analyzer = ConditionalVerboseAnalyzer()
|
|
78
|
+
conditional_calls = analyzer.find_conditional_verbose_calls(tree)
|
|
79
|
+
|
|
80
|
+
return self._collect_violations(conditional_calls, context)
|
|
81
|
+
|
|
82
|
+
def _should_analyze(self, context: BaseLintContext) -> bool:
|
|
83
|
+
"""Check if this file should be analyzed."""
|
|
84
|
+
if not has_file_content(context):
|
|
85
|
+
return False
|
|
86
|
+
if context.language != Language.PYTHON:
|
|
87
|
+
return False
|
|
88
|
+
config = self._load_config(context)
|
|
89
|
+
if not config.enabled:
|
|
90
|
+
return False
|
|
91
|
+
return not self._is_file_ignored(context, config)
|
|
92
|
+
|
|
93
|
+
def _load_config(self, context: BaseLintContext) -> PrintStatementConfig:
|
|
94
|
+
"""Load configuration from context.
|
|
95
|
+
|
|
96
|
+
Uses the same config as print-statements linter for consistency.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
context: Lint context
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
PrintStatementConfig instance
|
|
103
|
+
"""
|
|
104
|
+
test_config = self._try_load_test_config(context)
|
|
105
|
+
if test_config is not None:
|
|
106
|
+
return test_config
|
|
107
|
+
|
|
108
|
+
prod_config = self._try_load_production_config(context)
|
|
109
|
+
if prod_config is not None:
|
|
110
|
+
return prod_config
|
|
111
|
+
|
|
112
|
+
return PrintStatementConfig()
|
|
113
|
+
|
|
114
|
+
def _try_load_test_config(self, context: BaseLintContext) -> PrintStatementConfig | None:
|
|
115
|
+
"""Try to load test-style configuration."""
|
|
116
|
+
if not hasattr(context, "config"):
|
|
117
|
+
return None
|
|
118
|
+
config_attr = context.config
|
|
119
|
+
if config_attr is None or not isinstance(config_attr, dict):
|
|
120
|
+
return None
|
|
121
|
+
return PrintStatementConfig.from_dict(config_attr, context.language)
|
|
122
|
+
|
|
123
|
+
def _try_load_production_config(self, context: BaseLintContext) -> PrintStatementConfig | None:
|
|
124
|
+
"""Try to load production configuration."""
|
|
125
|
+
if not hasattr(context, "metadata") or not isinstance(context.metadata, dict):
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
metadata = context.metadata
|
|
129
|
+
config_keys = ("print_statements", "print-statements", "improper-logging")
|
|
130
|
+
|
|
131
|
+
for key in config_keys:
|
|
132
|
+
if key in metadata:
|
|
133
|
+
return load_linter_config(context, key, PrintStatementConfig)
|
|
134
|
+
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def _is_file_ignored(self, context: BaseLintContext, config: PrintStatementConfig) -> bool:
|
|
138
|
+
"""Check if file matches ignore patterns."""
|
|
139
|
+
if not config.ignore:
|
|
140
|
+
return False
|
|
141
|
+
if not context.file_path:
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
file_path = Path(context.file_path)
|
|
145
|
+
return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
|
|
146
|
+
|
|
147
|
+
def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
|
|
148
|
+
"""Check if file path matches a glob pattern."""
|
|
149
|
+
if file_path.match(pattern):
|
|
150
|
+
return True
|
|
151
|
+
if pattern in str(file_path):
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
def _parse_python_code(self, code: str | None) -> ast.AST | None:
|
|
156
|
+
"""Parse Python code into AST."""
|
|
157
|
+
try:
|
|
158
|
+
return ast.parse(code or "")
|
|
159
|
+
except SyntaxError:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
def _collect_violations(
|
|
163
|
+
self,
|
|
164
|
+
conditional_calls: list[tuple[ast.If, ast.Call, str, int]],
|
|
165
|
+
context: BaseLintContext,
|
|
166
|
+
) -> list[Violation]:
|
|
167
|
+
"""Collect violations from conditional verbose logging patterns.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
conditional_calls: List of (if_node, call_node, method_name, line_number) tuples
|
|
171
|
+
context: Lint context
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of violations
|
|
175
|
+
"""
|
|
176
|
+
violations = []
|
|
177
|
+
for _if_node, _call_node, method_name, line_number in conditional_calls:
|
|
178
|
+
violation = self._create_violation(method_name, line_number, context)
|
|
179
|
+
if not self._should_ignore(violation, context):
|
|
180
|
+
violations.append(violation)
|
|
181
|
+
return violations
|
|
182
|
+
|
|
183
|
+
def _create_violation(
|
|
184
|
+
self,
|
|
185
|
+
method_name: str,
|
|
186
|
+
line: int,
|
|
187
|
+
context: BaseLintContext,
|
|
188
|
+
) -> Violation:
|
|
189
|
+
"""Create a violation for a conditional verbose logging pattern.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
method_name: The logger method name (debug, info, etc.)
|
|
193
|
+
line: Line number where the violation occurs
|
|
194
|
+
context: Lint context
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Violation object with details about the pattern
|
|
198
|
+
"""
|
|
199
|
+
message = f"Conditional verbose check around logger.{method_name}() should be removed"
|
|
200
|
+
suggestion = (
|
|
201
|
+
"Remove the 'if verbose:' condition and configure logging level instead. "
|
|
202
|
+
"Use logger.setLevel(logging.DEBUG) to control verbosity."
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return Violation(
|
|
206
|
+
rule_id=self.rule_id,
|
|
207
|
+
file_path=str(context.file_path) if context.file_path else "",
|
|
208
|
+
line=line,
|
|
209
|
+
column=0,
|
|
210
|
+
message=message,
|
|
211
|
+
suggestion=suggestion,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
215
|
+
"""Check if violation should be ignored based on inline directives.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
violation: Violation to check
|
|
219
|
+
context: Lint context with file content
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
True if violation should be ignored
|
|
223
|
+
"""
|
|
224
|
+
if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
|
|
225
|
+
return True
|
|
226
|
+
return self._check_generic_ignore(violation, context)
|
|
227
|
+
|
|
228
|
+
def _check_generic_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
229
|
+
"""Check for generic ignore directives.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
violation: Violation to check
|
|
233
|
+
context: Lint context
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if line has generic ignore directive
|
|
237
|
+
"""
|
|
238
|
+
line_text = get_violation_line(violation, context)
|
|
239
|
+
if line_text is None:
|
|
240
|
+
return False
|
|
241
|
+
return self._has_generic_ignore_directive(line_text)
|
|
242
|
+
|
|
243
|
+
def _has_generic_ignore_directive(self, line_text: str) -> bool:
|
|
244
|
+
"""Check if line has generic ignore directive."""
|
|
245
|
+
if self._has_generic_thailint_ignore(line_text):
|
|
246
|
+
return True
|
|
247
|
+
return has_python_noqa(line_text)
|
|
248
|
+
|
|
249
|
+
def _has_generic_thailint_ignore(self, line_text: str) -> bool:
|
|
250
|
+
"""Check for generic thailint: ignore (no brackets)."""
|
|
251
|
+
if "# thailint: ignore" not in line_text:
|
|
252
|
+
return False
|
|
253
|
+
after_ignore = line_text.split("# thailint: ignore")[1].split("#")[0]
|
|
254
|
+
return "[" not in after_ignore
|
|
@@ -54,12 +54,12 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
54
54
|
@property
|
|
55
55
|
def rule_id(self) -> str:
|
|
56
56
|
"""Unique identifier for this rule."""
|
|
57
|
-
return "print-
|
|
57
|
+
return "improper-logging.print-statement"
|
|
58
58
|
|
|
59
59
|
@property
|
|
60
60
|
def rule_name(self) -> str:
|
|
61
61
|
"""Human-readable name for this rule."""
|
|
62
|
-
return "Print
|
|
62
|
+
return "Improper Logging - Print Statement"
|
|
63
63
|
|
|
64
64
|
@property
|
|
65
65
|
def description(self) -> str:
|
|
@@ -25,6 +25,7 @@ from src.core.types import Severity, Violation
|
|
|
25
25
|
|
|
26
26
|
from .config import SRPConfig
|
|
27
27
|
from .python_analyzer import PythonSRPAnalyzer
|
|
28
|
+
from .rust_analyzer import RustSRPAnalyzer
|
|
28
29
|
from .typescript_analyzer import TypeScriptSRPAnalyzer
|
|
29
30
|
|
|
30
31
|
|
|
@@ -36,6 +37,7 @@ class ClassAnalyzer:
|
|
|
36
37
|
# Singleton analyzers for performance (avoid recreating per-file)
|
|
37
38
|
self._python_analyzer = PythonSRPAnalyzer()
|
|
38
39
|
self._typescript_analyzer = TypeScriptSRPAnalyzer()
|
|
40
|
+
self._rust_analyzer = RustSRPAnalyzer()
|
|
39
41
|
|
|
40
42
|
def analyze_python(
|
|
41
43
|
self, context: BaseLintContext, config: SRPConfig
|
|
@@ -81,6 +83,53 @@ class ClassAnalyzer:
|
|
|
81
83
|
for class_node in classes
|
|
82
84
|
]
|
|
83
85
|
|
|
86
|
+
def analyze_rust(self, context: BaseLintContext, config: SRPConfig) -> list[dict[str, Any]]:
|
|
87
|
+
"""Analyze Rust structs and return metrics.
|
|
88
|
+
|
|
89
|
+
Finds all struct declarations and impl blocks, matches impl blocks to
|
|
90
|
+
their target structs, and returns metrics for each struct.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
context: Lint context with file information
|
|
94
|
+
config: SRP configuration
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of struct metrics dicts
|
|
98
|
+
"""
|
|
99
|
+
root_node = self._rust_analyzer.parse_rust(context.file_content or "")
|
|
100
|
+
if not root_node:
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
structs = self._rust_analyzer.find_all_structs(root_node)
|
|
104
|
+
impl_blocks = self._rust_analyzer.find_all_impl_blocks(root_node)
|
|
105
|
+
impl_map = self._build_impl_map(impl_blocks)
|
|
106
|
+
|
|
107
|
+
return [
|
|
108
|
+
self._rust_analyzer.analyze_struct(
|
|
109
|
+
struct_node,
|
|
110
|
+
impl_map.get(self._rust_analyzer.get_impl_target_name(struct_node), []),
|
|
111
|
+
context.file_content or "",
|
|
112
|
+
config,
|
|
113
|
+
)
|
|
114
|
+
for struct_node in structs
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
def _build_impl_map(self, impl_blocks: list) -> dict[str, list]:
|
|
118
|
+
"""Build mapping from struct name to its impl blocks.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
impl_blocks: List of impl_item nodes
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dictionary mapping struct names to lists of impl blocks
|
|
125
|
+
"""
|
|
126
|
+
impl_map: dict[str, list] = {}
|
|
127
|
+
for impl_node in impl_blocks:
|
|
128
|
+
target_name = self._rust_analyzer.get_impl_target_name(impl_node)
|
|
129
|
+
if target_name:
|
|
130
|
+
impl_map.setdefault(target_name, []).append(impl_node)
|
|
131
|
+
return impl_map
|
|
132
|
+
|
|
84
133
|
def _parse_python_safely(self, context: BaseLintContext) -> ast.AST | list[Violation]:
|
|
85
134
|
"""Parse Python code and return AST or syntax error violations.
|
|
86
135
|
|
src/linters/srp/linter.py
CHANGED
|
@@ -21,6 +21,7 @@ Suppressions:
|
|
|
21
21
|
- type:ignore[return-value]: Generic TypeScript analyzer return type variance
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
from src.analyzers.rust_base import TREE_SITTER_RUST_AVAILABLE
|
|
24
25
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
25
26
|
from src.core.constants import Language
|
|
26
27
|
from src.core.linter_utils import load_linter_config
|
|
@@ -110,6 +111,9 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
110
111
|
if context.language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
|
|
111
112
|
return self._check_typescript(context, config)
|
|
112
113
|
|
|
114
|
+
if context.language == Language.RUST:
|
|
115
|
+
return self._check_rust(context, config)
|
|
116
|
+
|
|
113
117
|
return []
|
|
114
118
|
|
|
115
119
|
def _load_config(self, context: BaseLintContext) -> SRPConfig:
|
|
@@ -204,6 +208,24 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
204
208
|
|
|
205
209
|
return violation
|
|
206
210
|
|
|
211
|
+
def _check_rust(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
212
|
+
"""Check Rust code for SRP violations.
|
|
213
|
+
|
|
214
|
+
Analyzes struct declarations and their impl blocks to detect structs
|
|
215
|
+
with too many methods, excessive lines of code, or generic naming.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
context: Lint context with file information
|
|
219
|
+
config: SRP configuration
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of violations found
|
|
223
|
+
"""
|
|
224
|
+
if not TREE_SITTER_RUST_AVAILABLE:
|
|
225
|
+
return []
|
|
226
|
+
metrics_list = self._class_analyzer.analyze_rust(context, config)
|
|
227
|
+
return self._build_violations_from_metrics(metrics_list, config, context)
|
|
228
|
+
|
|
207
229
|
def _check_typescript(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
208
230
|
"""Check TypeScript code for SRP violations.
|
|
209
231
|
|