thailint 0.16.0__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/linters/__init__.py +8 -1
- src/cli/linters/rust.py +177 -0
- src/core/base.py +30 -0
- src/core/constants.py +1 -0
- src/core/linter_utils.py +42 -1
- 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/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.16.0.dist-info → thailint-0.17.0.dist-info}/METADATA +5 -2
- {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/RECORD +35 -16
- {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/WHEEL +0 -0
- {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Main linter rule for detecting clone abuse in Rust code
|
|
3
|
+
|
|
4
|
+
Scope: Entry point for clone abuse detection implementing BaseLintRule interface
|
|
5
|
+
|
|
6
|
+
Overview: Provides CloneAbuseRule class that implements the BaseLintRule interface for
|
|
7
|
+
detecting .clone() abuse patterns in Rust code. Validates that files are Rust with
|
|
8
|
+
content, loads configuration, checks ignored paths, and delegates analysis to
|
|
9
|
+
RustCloneAnalyzer. Filters detected calls based on configuration (allow_in_tests,
|
|
10
|
+
pattern toggles, ignored paths) and converts remaining calls to Violation objects
|
|
11
|
+
via the violation builder. Supports disabling via configuration.
|
|
12
|
+
|
|
13
|
+
Dependencies: BaseLintRule, RustCloneAnalyzer, CloneAbuseConfig, violation_builder
|
|
14
|
+
|
|
15
|
+
Exports: CloneAbuseRule
|
|
16
|
+
|
|
17
|
+
Interfaces: check(context: BaseLintContext) -> list[Violation]
|
|
18
|
+
|
|
19
|
+
Implementation: Single-file analysis with config-driven filtering and tree-sitter-based detection
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from src.core.base import BaseLintContext, BaseLintRule
|
|
23
|
+
from src.core.linter_utils import (
|
|
24
|
+
has_file_content,
|
|
25
|
+
is_ignored_path,
|
|
26
|
+
load_linter_config,
|
|
27
|
+
resolve_file_path,
|
|
28
|
+
)
|
|
29
|
+
from src.core.types import Violation
|
|
30
|
+
|
|
31
|
+
from .config import CloneAbuseConfig
|
|
32
|
+
from .rust_analyzer import CloneCall, RustCloneAnalyzer
|
|
33
|
+
from .violation_builder import (
|
|
34
|
+
build_clone_chain_violation,
|
|
35
|
+
build_clone_in_loop_violation,
|
|
36
|
+
build_unnecessary_clone_violation,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_PATTERN_BUILDERS = {
|
|
40
|
+
"clone-in-loop": build_clone_in_loop_violation,
|
|
41
|
+
"clone-chain": build_clone_chain_violation,
|
|
42
|
+
"unnecessary-clone": build_unnecessary_clone_violation,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_PATTERN_CONFIG_KEYS = {
|
|
46
|
+
"clone-in-loop": "detect_clone_in_loop",
|
|
47
|
+
"clone-chain": "detect_clone_chain",
|
|
48
|
+
"unnecessary-clone": "detect_unnecessary_clone",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CloneAbuseRule(BaseLintRule):
|
|
53
|
+
"""Detects clone abuse patterns in Rust code."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: CloneAbuseConfig | None = None) -> None:
|
|
56
|
+
"""Initialize clone abuse rule.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
config: Optional configuration override for testing
|
|
60
|
+
"""
|
|
61
|
+
self._config_override = config
|
|
62
|
+
self._analyzer = RustCloneAnalyzer()
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def rule_id(self) -> str:
|
|
66
|
+
"""Unique identifier for this rule."""
|
|
67
|
+
return "clone-abuse"
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def rule_name(self) -> str:
|
|
71
|
+
"""Human-readable name for this rule."""
|
|
72
|
+
return "Rust Clone Abuse"
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def description(self) -> str:
|
|
76
|
+
"""Description of what this rule checks."""
|
|
77
|
+
return (
|
|
78
|
+
"Detects .clone() abuse patterns in Rust code including clone in loops, "
|
|
79
|
+
"chained clones, and unnecessary clones. Suggests safer alternatives like "
|
|
80
|
+
"borrowing, Rc/Arc, or Cow patterns."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
84
|
+
"""Check for clone abuse violations in a Rust file.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
context: Lint context with file content and metadata
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
List of violations found
|
|
91
|
+
"""
|
|
92
|
+
config = self._get_config(context)
|
|
93
|
+
if not self._should_analyze(context, config):
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
file_path = resolve_file_path(context)
|
|
97
|
+
calls = self._analyzer.find_clone_calls(context.file_content or "")
|
|
98
|
+
return self._build_violations(calls, config, file_path)
|
|
99
|
+
|
|
100
|
+
def _should_analyze(self, context: BaseLintContext, config: CloneAbuseConfig) -> bool:
|
|
101
|
+
"""Determine if the file should be analyzed.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
context: Lint context to check
|
|
105
|
+
config: Clone abuse configuration
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if the file should be analyzed
|
|
109
|
+
"""
|
|
110
|
+
if context.language != "rust":
|
|
111
|
+
return False
|
|
112
|
+
if not has_file_content(context):
|
|
113
|
+
return False
|
|
114
|
+
if not config.enabled:
|
|
115
|
+
return False
|
|
116
|
+
return not is_ignored_path(resolve_file_path(context), config.ignore)
|
|
117
|
+
|
|
118
|
+
def _get_config(self, context: BaseLintContext) -> CloneAbuseConfig:
|
|
119
|
+
"""Load configuration from override or context metadata.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
context: Lint context with metadata
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
CloneAbuseConfig instance
|
|
126
|
+
"""
|
|
127
|
+
if self._config_override is not None:
|
|
128
|
+
return self._config_override
|
|
129
|
+
return load_linter_config(context, "clone-abuse", CloneAbuseConfig)
|
|
130
|
+
|
|
131
|
+
def _build_violations(
|
|
132
|
+
self,
|
|
133
|
+
calls: list[CloneCall],
|
|
134
|
+
config: CloneAbuseConfig,
|
|
135
|
+
file_path: str,
|
|
136
|
+
) -> list[Violation]:
|
|
137
|
+
"""Convert clone calls to violations with config filtering.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
calls: Detected clone calls from analyzer
|
|
141
|
+
config: Configuration for filtering
|
|
142
|
+
file_path: Path of the analyzed file
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
List of filtered violations
|
|
146
|
+
"""
|
|
147
|
+
return [
|
|
148
|
+
_build_violation_for_call(call, file_path)
|
|
149
|
+
for call in calls
|
|
150
|
+
if not _should_skip_call(call, config)
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _should_skip_call(call: CloneCall, config: CloneAbuseConfig) -> bool:
|
|
155
|
+
"""Determine if a clone call should be skipped based on config.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
call: Detected clone call
|
|
159
|
+
config: Configuration for filtering
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if the call should be skipped
|
|
163
|
+
"""
|
|
164
|
+
if call.is_in_test and config.allow_in_tests:
|
|
165
|
+
return True
|
|
166
|
+
config_key = _PATTERN_CONFIG_KEYS.get(call.pattern)
|
|
167
|
+
if config_key and not getattr(config, config_key):
|
|
168
|
+
return True
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _build_violation_for_call(call: CloneCall, file_path: str) -> Violation:
|
|
173
|
+
"""Build a violation for a specific clone call.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
call: Detected clone call with pattern info
|
|
177
|
+
file_path: Path of the analyzed file
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Violation instance
|
|
181
|
+
"""
|
|
182
|
+
builder = _PATTERN_BUILDERS.get(call.pattern, build_clone_in_loop_violation)
|
|
183
|
+
return builder(file_path, call.line, call.column, call.context)
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Analyzer for detecting clone abuse patterns in Rust code using tree-sitter AST
|
|
3
|
+
|
|
4
|
+
Scope: Pattern detection for .clone() method calls including loop, chain, and unnecessary patterns
|
|
5
|
+
|
|
6
|
+
Overview: Provides RustCloneAnalyzer that extends RustBaseAnalyzer to detect .clone() abuse
|
|
7
|
+
patterns in Rust code. Detects three patterns: clone-in-loop (clone calls inside for,
|
|
8
|
+
while, or loop bodies), clone-chain (chained .clone().clone() calls), and unnecessary-clone
|
|
9
|
+
(clone in let binding where the source identifier is not used again in the enclosing block).
|
|
10
|
+
Uses tree-sitter AST to find call_expression nodes with field_identifier matching "clone"
|
|
11
|
+
and classifies each call by walking the AST structure. Returns structured CloneCall dataclass
|
|
12
|
+
instances with location, pattern type, test context, and surrounding code for violation reporting.
|
|
13
|
+
|
|
14
|
+
Dependencies: src.analyzers.rust_base for tree-sitter parsing and traversal
|
|
15
|
+
|
|
16
|
+
Exports: RustCloneAnalyzer, CloneCall
|
|
17
|
+
|
|
18
|
+
Interfaces: find_clone_calls(code: str) -> list[CloneCall]
|
|
19
|
+
|
|
20
|
+
Implementation: Recursive AST traversal with pattern classification using parent-chain walking
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
from src.analyzers.rust_base import RustBaseAnalyzer
|
|
29
|
+
from src.core.linter_utils import get_line_context
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from tree_sitter import Node
|
|
33
|
+
|
|
34
|
+
_LOOP_NODE_TYPES = frozenset({"for_expression", "while_expression", "loop_expression"})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class CloneCall:
|
|
39
|
+
"""Represents a detected clone call with its abuse pattern."""
|
|
40
|
+
|
|
41
|
+
line: int
|
|
42
|
+
column: int
|
|
43
|
+
pattern: str # "clone-in-loop", "clone-chain", "unnecessary-clone"
|
|
44
|
+
is_in_test: bool
|
|
45
|
+
context: str # Surrounding code snippet
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RustCloneAnalyzer(RustBaseAnalyzer):
|
|
49
|
+
"""Analyzer for detecting clone abuse patterns in Rust code."""
|
|
50
|
+
|
|
51
|
+
def find_clone_calls(self, code: str) -> list[CloneCall]:
|
|
52
|
+
"""Find all abusive .clone() calls in code.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
code: Rust source code to analyze
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of detected clone calls with pattern classification
|
|
59
|
+
"""
|
|
60
|
+
if not self.tree_sitter_available:
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
root = self.parse_rust(code)
|
|
64
|
+
if root is None:
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
calls: list[CloneCall] = []
|
|
68
|
+
self._find_clone_recursive(root, code, calls)
|
|
69
|
+
return calls
|
|
70
|
+
|
|
71
|
+
def _find_clone_recursive(self, node: Node, code: str, calls: list[CloneCall]) -> None:
|
|
72
|
+
"""Recursively find abusive clone calls in AST.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
node: Current tree-sitter node to inspect
|
|
76
|
+
code: Original source code for context extraction
|
|
77
|
+
calls: Accumulator list for detected calls
|
|
78
|
+
"""
|
|
79
|
+
if node.type == "call_expression":
|
|
80
|
+
method_name = self._get_method_name(node)
|
|
81
|
+
if method_name == "clone":
|
|
82
|
+
pattern = self._classify_clone(node, code)
|
|
83
|
+
if pattern is not None:
|
|
84
|
+
calls.append(
|
|
85
|
+
CloneCall(
|
|
86
|
+
line=node.start_point[0] + 1,
|
|
87
|
+
column=node.start_point[1],
|
|
88
|
+
pattern=pattern,
|
|
89
|
+
is_in_test=self.is_inside_test(node),
|
|
90
|
+
context=get_line_context(code, node.start_point[0]),
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
for child in node.children:
|
|
95
|
+
self._find_clone_recursive(child, code, calls)
|
|
96
|
+
|
|
97
|
+
def _get_method_name(self, call_node: Node) -> str:
|
|
98
|
+
"""Extract method name from a call expression.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
call_node: A call_expression node
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Method name string, or empty string if not a method call
|
|
105
|
+
"""
|
|
106
|
+
for child in call_node.children:
|
|
107
|
+
if child.type == "field_expression":
|
|
108
|
+
return self._extract_field_identifier(child)
|
|
109
|
+
return ""
|
|
110
|
+
|
|
111
|
+
def _extract_field_identifier(self, field_expr: Node) -> str:
|
|
112
|
+
"""Extract the field identifier from a field expression.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
field_expr: A field_expression node
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
The field identifier text (e.g., "clone")
|
|
119
|
+
"""
|
|
120
|
+
for subchild in field_expr.children:
|
|
121
|
+
if subchild.type == "field_identifier":
|
|
122
|
+
return self.extract_node_text(subchild)
|
|
123
|
+
return ""
|
|
124
|
+
|
|
125
|
+
def _classify_clone(self, node: Node, code: str) -> str | None:
|
|
126
|
+
"""Classify a clone call into an abuse pattern.
|
|
127
|
+
|
|
128
|
+
Checks patterns in priority order: chain, loop, unnecessary.
|
|
129
|
+
Returns None if the clone does not match any abuse pattern.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
node: The call_expression node for the .clone() call
|
|
133
|
+
code: Original source code
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Pattern string or None if not abusive
|
|
137
|
+
"""
|
|
138
|
+
_ = code
|
|
139
|
+
if self._is_chained_clone(node):
|
|
140
|
+
return "clone-chain"
|
|
141
|
+
if self._is_inside_loop(node):
|
|
142
|
+
return "clone-in-loop"
|
|
143
|
+
if self._is_unnecessary_clone(node):
|
|
144
|
+
return "unnecessary-clone"
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def _is_inside_loop(self, node: Node) -> bool:
|
|
148
|
+
"""Check if node is inside a loop body.
|
|
149
|
+
|
|
150
|
+
Walks the parent chain looking for loop node types.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
node: Node to check
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
True if inside a for, while, or loop expression
|
|
157
|
+
"""
|
|
158
|
+
current: Node | None = node.parent
|
|
159
|
+
while current is not None:
|
|
160
|
+
if current.type in _LOOP_NODE_TYPES:
|
|
161
|
+
return True
|
|
162
|
+
current = current.parent
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
def _is_chained_clone(self, node: Node) -> bool:
|
|
166
|
+
"""Check if this clone's receiver is itself a .clone() call.
|
|
167
|
+
|
|
168
|
+
Detects patterns like data.clone().clone() where the outer clone's
|
|
169
|
+
receiver (via field_expression) is a call_expression with method "clone".
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
node: The call_expression node for this .clone() call
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
True if this clone is chained on another clone
|
|
176
|
+
"""
|
|
177
|
+
field_expr = _get_field_expression(node)
|
|
178
|
+
if field_expr is None:
|
|
179
|
+
return False
|
|
180
|
+
receiver = _get_receiver_node(field_expr)
|
|
181
|
+
if receiver is None or receiver.type != "call_expression":
|
|
182
|
+
return False
|
|
183
|
+
return self._get_method_name(receiver) == "clone"
|
|
184
|
+
|
|
185
|
+
def _is_unnecessary_clone(self, node: Node) -> bool:
|
|
186
|
+
"""Check if clone is unnecessary (source not used after cloning).
|
|
187
|
+
|
|
188
|
+
Only flags when ALL conditions are met:
|
|
189
|
+
- The clone is in a let declaration: let x = y.clone();
|
|
190
|
+
- The receiver y is a simple identifier (not self.field, not foo.bar())
|
|
191
|
+
- y does not appear in any subsequent statement in the same block
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
node: The call_expression node for this .clone() call
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
True if the clone appears unnecessary
|
|
198
|
+
"""
|
|
199
|
+
let_node = _find_parent_let_declaration(node)
|
|
200
|
+
if let_node is None:
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
identifier = self._get_clone_receiver_identifier(node)
|
|
204
|
+
if identifier is None:
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
block_node = _find_parent_block(let_node)
|
|
208
|
+
if block_node is None:
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
return not _identifier_used_after(identifier, let_node, block_node)
|
|
212
|
+
|
|
213
|
+
def _get_clone_receiver_identifier(self, node: Node) -> str | None:
|
|
214
|
+
"""Extract the simple identifier being cloned.
|
|
215
|
+
|
|
216
|
+
Returns None for complex expressions like self.field or foo.bar().
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
node: The call_expression node for this .clone() call
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Simple identifier string, or None for complex receivers
|
|
223
|
+
"""
|
|
224
|
+
field_expr = _get_field_expression(node)
|
|
225
|
+
if field_expr is None:
|
|
226
|
+
return None
|
|
227
|
+
receiver = _get_receiver_node(field_expr)
|
|
228
|
+
if receiver is None:
|
|
229
|
+
return None
|
|
230
|
+
if receiver.type != "identifier":
|
|
231
|
+
return None
|
|
232
|
+
return self.extract_node_text(receiver)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _get_field_expression(call_node: Node) -> Node | None:
|
|
236
|
+
"""Get the field_expression child of a call_expression.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
call_node: A call_expression node
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
The field_expression child, or None
|
|
243
|
+
"""
|
|
244
|
+
for child in call_node.children:
|
|
245
|
+
if child.type == "field_expression":
|
|
246
|
+
return child
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _get_receiver_node(field_expr: Node) -> Node | None:
|
|
251
|
+
"""Get the receiver (first child) of a field_expression.
|
|
252
|
+
|
|
253
|
+
In Rust tree-sitter, field_expression children are:
|
|
254
|
+
[receiver, ".", field_identifier]
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
field_expr: A field_expression node
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
The receiver node, or None
|
|
261
|
+
"""
|
|
262
|
+
children = field_expr.children
|
|
263
|
+
if children:
|
|
264
|
+
return children[0]
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _find_parent_let_declaration(node: Node) -> Node | None:
|
|
269
|
+
"""Find the enclosing let_declaration for a node.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
node: Starting node to search from
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
The let_declaration node, or None if not inside one
|
|
276
|
+
"""
|
|
277
|
+
current: Node | None = node.parent
|
|
278
|
+
while current is not None:
|
|
279
|
+
if current.type == "let_declaration":
|
|
280
|
+
return current
|
|
281
|
+
if current.type in ("block", "function_item"):
|
|
282
|
+
return None
|
|
283
|
+
current = current.parent
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _find_parent_block(node: Node) -> Node | None:
|
|
288
|
+
"""Find the enclosing block for a node.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
node: Starting node to search from
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
The block node, or None
|
|
295
|
+
"""
|
|
296
|
+
current: Node | None = node.parent
|
|
297
|
+
while current is not None:
|
|
298
|
+
if current.type == "block":
|
|
299
|
+
return current
|
|
300
|
+
current = current.parent
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _identifier_used_after(identifier: str, let_node: Node, block_node: Node) -> bool:
|
|
305
|
+
"""Check if identifier appears in statements after let_node in the block.
|
|
306
|
+
|
|
307
|
+
Scans subsequent siblings of let_node within block_node for any reference
|
|
308
|
+
to the given identifier.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
identifier: The variable name to search for
|
|
312
|
+
let_node: The let_declaration node
|
|
313
|
+
block_node: The enclosing block node
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
True if identifier is referenced after the let_node
|
|
317
|
+
"""
|
|
318
|
+
found_let = False
|
|
319
|
+
for child in block_node.children:
|
|
320
|
+
if child.id == let_node.id:
|
|
321
|
+
found_let = True
|
|
322
|
+
continue
|
|
323
|
+
if found_let and _node_contains_identifier(child, identifier):
|
|
324
|
+
return True
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _node_contains_identifier(node: Node, identifier: str) -> bool:
|
|
329
|
+
"""Recursively check if a node or its descendants contain an identifier reference.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
node: Node to search in
|
|
333
|
+
identifier: Identifier name to look for
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
True if the identifier is found
|
|
337
|
+
"""
|
|
338
|
+
if _is_matching_identifier(node, identifier):
|
|
339
|
+
return True
|
|
340
|
+
return any(_node_contains_identifier(child, identifier) for child in node.children)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _is_matching_identifier(node: Node, identifier: str) -> bool:
|
|
344
|
+
"""Check if a node is an identifier matching the given name.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
node: Node to check
|
|
348
|
+
identifier: Expected identifier name
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
True if node is an identifier with the given name
|
|
352
|
+
"""
|
|
353
|
+
if node.type != "identifier":
|
|
354
|
+
return False
|
|
355
|
+
text = node.text
|
|
356
|
+
return text is not None and text.decode() == identifier
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Build Violation objects for Rust clone abuse patterns
|
|
3
|
+
|
|
4
|
+
Scope: Creates violations with actionable suggestions for clone-in-loop, clone-chain,
|
|
5
|
+
and unnecessary-clone patterns
|
|
6
|
+
|
|
7
|
+
Overview: Provides module-level functions that create Violation objects for detected
|
|
8
|
+
.clone() abuse patterns in Rust code. Each violation includes the rule ID, location,
|
|
9
|
+
descriptive message explaining the performance or correctness impact, and a suggestion
|
|
10
|
+
for safer alternatives such as borrowing, Rc/Arc for shared ownership, or Cow for
|
|
11
|
+
clone-on-write patterns.
|
|
12
|
+
|
|
13
|
+
Dependencies: src.core.types for Violation dataclass
|
|
14
|
+
|
|
15
|
+
Exports: build_clone_in_loop_violation, build_clone_chain_violation, build_unnecessary_clone_violation
|
|
16
|
+
|
|
17
|
+
Interfaces: Module functions taking file_path, line, column, context and returning Violation
|
|
18
|
+
|
|
19
|
+
Implementation: Factory functions for each clone abuse pattern with pattern-specific suggestions
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from src.core.types import Violation
|
|
23
|
+
|
|
24
|
+
_CLONE_IN_LOOP_SUGGESTION = (
|
|
25
|
+
"Consider borrowing instead of cloning in a loop. "
|
|
26
|
+
"If ownership is needed, use Rc/Arc for shared ownership or collect references."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_CLONE_CHAIN_SUGGESTION = (
|
|
30
|
+
"Chained .clone().clone() is redundant. "
|
|
31
|
+
"A single .clone() produces an owned copy; the second clone is unnecessary."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_UNNECESSARY_CLONE_SUGGESTION = (
|
|
35
|
+
"This .clone() may be unnecessary if the original value is not used after cloning. "
|
|
36
|
+
"Consider passing ownership directly, borrowing, or using Cow for clone-on-write."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_clone_in_loop_violation(
|
|
41
|
+
file_path: str,
|
|
42
|
+
line: int,
|
|
43
|
+
column: int,
|
|
44
|
+
context: str,
|
|
45
|
+
) -> Violation:
|
|
46
|
+
"""Build a violation for .clone() call inside a loop body."""
|
|
47
|
+
message = f".clone() called inside a loop body may cause performance issues: {context}"
|
|
48
|
+
|
|
49
|
+
return Violation(
|
|
50
|
+
rule_id="clone-abuse.clone-in-loop",
|
|
51
|
+
file_path=file_path,
|
|
52
|
+
line=line,
|
|
53
|
+
column=column,
|
|
54
|
+
message=message,
|
|
55
|
+
suggestion=_CLONE_IN_LOOP_SUGGESTION,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_clone_chain_violation(
|
|
60
|
+
file_path: str,
|
|
61
|
+
line: int,
|
|
62
|
+
column: int,
|
|
63
|
+
context: str,
|
|
64
|
+
) -> Violation:
|
|
65
|
+
"""Build a violation for chained .clone().clone() calls."""
|
|
66
|
+
message = f"Chained .clone().clone() is redundant: {context}"
|
|
67
|
+
|
|
68
|
+
return Violation(
|
|
69
|
+
rule_id="clone-abuse.clone-chain",
|
|
70
|
+
file_path=file_path,
|
|
71
|
+
line=line,
|
|
72
|
+
column=column,
|
|
73
|
+
message=message,
|
|
74
|
+
suggestion=_CLONE_CHAIN_SUGGESTION,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_unnecessary_clone_violation(
|
|
79
|
+
file_path: str,
|
|
80
|
+
line: int,
|
|
81
|
+
column: int,
|
|
82
|
+
context: str,
|
|
83
|
+
) -> Violation:
|
|
84
|
+
"""Build a violation for unnecessary .clone() before move."""
|
|
85
|
+
message = f".clone() may be unnecessary when the original is not used afterward: {context}"
|
|
86
|
+
|
|
87
|
+
return Violation(
|
|
88
|
+
rule_id="clone-abuse.unnecessary-clone",
|
|
89
|
+
file_path=file_path,
|
|
90
|
+
line=line,
|
|
91
|
+
column=column,
|
|
92
|
+
message=message,
|
|
93
|
+
suggestion=_UNNECESSARY_CLONE_SUGGESTION,
|
|
94
|
+
)
|