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,419 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Analyzer for detecting blocking operations inside async functions in Rust code
|
|
3
|
+
|
|
4
|
+
Scope: Pattern detection for std::fs, std::thread::sleep, and std::net calls in async contexts
|
|
5
|
+
|
|
6
|
+
Overview: Provides RustBlockingAsyncAnalyzer that extends RustBaseAnalyzer to detect blocking
|
|
7
|
+
API calls inside async functions in Rust code. Detects three categories: filesystem operations
|
|
8
|
+
(std::fs::read_to_string, std::fs::write, etc.), thread sleep (std::thread::sleep), and
|
|
9
|
+
blocking network calls (std::net::TcpStream::connect, etc.). Supports both fully-qualified
|
|
10
|
+
paths (std::fs::read_to_string) and short paths (fs::read_to_string after use std::fs).
|
|
11
|
+
Excludes blocking calls wrapped in async-safe wrappers (asyncify, spawn_blocking,
|
|
12
|
+
block_in_place) which correctly offload work to a thread pool. Uses tree-sitter AST to
|
|
13
|
+
find function_item nodes, filter to async functions, walk bodies for call_expression nodes
|
|
14
|
+
with scoped_identifier paths, and match against known blocking API patterns. Returns
|
|
15
|
+
structured BlockingCall dataclass instances with location, pattern type, test context, and
|
|
16
|
+
surrounding code for violation reporting.
|
|
17
|
+
|
|
18
|
+
Dependencies: src.analyzers.rust_base for tree-sitter parsing and traversal
|
|
19
|
+
|
|
20
|
+
Exports: RustBlockingAsyncAnalyzer, BlockingCall
|
|
21
|
+
|
|
22
|
+
Interfaces: find_blocking_calls(code: str) -> list[BlockingCall]
|
|
23
|
+
|
|
24
|
+
Implementation: AST-based async function detection with scoped_identifier path extraction
|
|
25
|
+
and pattern matching against known blocking APIs
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from typing import TYPE_CHECKING
|
|
32
|
+
|
|
33
|
+
from src.analyzers.rust_base import RustBaseAnalyzer
|
|
34
|
+
from src.core.linter_utils import get_line_context
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from tree_sitter import Node
|
|
38
|
+
|
|
39
|
+
# Blocking std::fs function names
|
|
40
|
+
_BLOCKING_FS_FUNCTIONS = frozenset(
|
|
41
|
+
{
|
|
42
|
+
"read_to_string",
|
|
43
|
+
"read",
|
|
44
|
+
"write",
|
|
45
|
+
"create_dir",
|
|
46
|
+
"create_dir_all",
|
|
47
|
+
"remove_file",
|
|
48
|
+
"remove_dir",
|
|
49
|
+
"remove_dir_all",
|
|
50
|
+
"rename",
|
|
51
|
+
"copy",
|
|
52
|
+
"metadata",
|
|
53
|
+
"read_dir",
|
|
54
|
+
"canonicalize",
|
|
55
|
+
"read_link",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Blocking std::net type names that have blocking methods
|
|
60
|
+
_BLOCKING_NET_TYPES = frozenset(
|
|
61
|
+
{
|
|
62
|
+
"TcpStream",
|
|
63
|
+
"TcpListener",
|
|
64
|
+
"UdpSocket",
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class BlockingCall:
|
|
71
|
+
"""Represents a detected blocking call inside an async function."""
|
|
72
|
+
|
|
73
|
+
line: int
|
|
74
|
+
column: int
|
|
75
|
+
pattern: str # "fs-in-async", "sleep-in-async", "net-in-async"
|
|
76
|
+
is_in_test: bool
|
|
77
|
+
context: str # Surrounding code snippet
|
|
78
|
+
blocking_api: str # e.g., "std::fs::read_to_string"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class RustBlockingAsyncAnalyzer(RustBaseAnalyzer):
|
|
82
|
+
"""Analyzer for detecting blocking operations inside async functions."""
|
|
83
|
+
|
|
84
|
+
def find_blocking_calls(self, code: str) -> list[BlockingCall]:
|
|
85
|
+
"""Find all blocking calls inside async functions.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
code: Rust source code to analyze
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of detected blocking calls with pattern classification
|
|
92
|
+
"""
|
|
93
|
+
if not self.tree_sitter_available:
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
root = self.parse_rust(code)
|
|
97
|
+
if root is None:
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
calls: list[BlockingCall] = []
|
|
101
|
+
self._scan_for_blocking_calls(root, code, calls)
|
|
102
|
+
return calls
|
|
103
|
+
|
|
104
|
+
def _scan_for_blocking_calls(self, node: Node, code: str, calls: list[BlockingCall]) -> None:
|
|
105
|
+
"""Recursively scan AST for blocking calls in async contexts.
|
|
106
|
+
|
|
107
|
+
Finds call_expression nodes inside async functions and checks if they
|
|
108
|
+
invoke known blocking APIs.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
node: Current tree-sitter node to inspect
|
|
112
|
+
code: Original source code for context extraction
|
|
113
|
+
calls: Accumulator list for detected calls
|
|
114
|
+
"""
|
|
115
|
+
if node.type == "call_expression" and self._is_in_async_context(node):
|
|
116
|
+
blocking_call = self._check_blocking_call(node, code)
|
|
117
|
+
if blocking_call is not None:
|
|
118
|
+
calls.append(blocking_call)
|
|
119
|
+
|
|
120
|
+
for child in node.children:
|
|
121
|
+
self._scan_for_blocking_calls(child, code, calls)
|
|
122
|
+
|
|
123
|
+
def _is_in_async_context(self, node: Node) -> bool:
|
|
124
|
+
"""Check if node is inside an async function body.
|
|
125
|
+
|
|
126
|
+
Walks up the parent chain looking for function_item nodes that
|
|
127
|
+
are async functions.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
node: Node to check
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
True if inside an async function
|
|
134
|
+
"""
|
|
135
|
+
current: Node | None = node.parent
|
|
136
|
+
while current is not None:
|
|
137
|
+
if current.type == "function_item" and self.is_async_function(current):
|
|
138
|
+
return True
|
|
139
|
+
current = current.parent
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
def _check_blocking_call(self, call_node: Node, code: str) -> BlockingCall | None:
|
|
143
|
+
"""Check if a call expression is a blocking API call.
|
|
144
|
+
|
|
145
|
+
Extracts the call path from the scoped_identifier child and matches
|
|
146
|
+
it against known blocking API patterns. Skips calls wrapped in
|
|
147
|
+
spawn_blocking/asyncify which are correctly offloaded to a thread pool.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
call_node: A call_expression node
|
|
151
|
+
code: Original source code for context extraction
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
BlockingCall if blocking API detected, None otherwise
|
|
155
|
+
"""
|
|
156
|
+
path = self._extract_call_path(call_node)
|
|
157
|
+
if not path:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
pattern = _classify_blocking_pattern(path)
|
|
161
|
+
if pattern is None:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
if _is_inside_blocking_wrapper(call_node):
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
return BlockingCall(
|
|
168
|
+
line=call_node.start_point[0] + 1,
|
|
169
|
+
column=call_node.start_point[1],
|
|
170
|
+
pattern=pattern,
|
|
171
|
+
is_in_test=self.is_inside_test(call_node),
|
|
172
|
+
context=get_line_context(code, call_node.start_point[0]),
|
|
173
|
+
blocking_api=path,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def _extract_call_path(self, call_node: Node) -> str:
|
|
177
|
+
"""Extract the full call path from a call_expression.
|
|
178
|
+
|
|
179
|
+
Handles both direct scoped calls (std::fs::read_to_string(...))
|
|
180
|
+
and method-style calls that chain on scoped calls.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
call_node: A call_expression node
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Full path string (e.g., "std::fs::read_to_string"), or empty string
|
|
187
|
+
"""
|
|
188
|
+
for child in call_node.children:
|
|
189
|
+
if child.type == "scoped_identifier":
|
|
190
|
+
return self.extract_node_text(child)
|
|
191
|
+
return ""
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _classify_blocking_pattern(path: str) -> str | None:
|
|
195
|
+
"""Classify a call path into a blocking pattern category.
|
|
196
|
+
|
|
197
|
+
Checks the path against known blocking API patterns for filesystem,
|
|
198
|
+
thread sleep, and network operations.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
path: Full or short call path (e.g., "std::fs::read_to_string")
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Pattern string or None if not a blocking pattern
|
|
205
|
+
"""
|
|
206
|
+
if _is_blocking_fs(path):
|
|
207
|
+
return "fs-in-async"
|
|
208
|
+
if _is_blocking_sleep(path):
|
|
209
|
+
return "sleep-in-async"
|
|
210
|
+
if _is_blocking_net(path):
|
|
211
|
+
return "net-in-async"
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _is_blocking_fs(path: str) -> bool:
|
|
216
|
+
"""Check if path matches a blocking filesystem operation.
|
|
217
|
+
|
|
218
|
+
Matches both fully-qualified (std::fs::read_to_string) and
|
|
219
|
+
short paths (fs::read_to_string).
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
path: Call path to check
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if path is a blocking fs operation
|
|
226
|
+
"""
|
|
227
|
+
parts = path.split("::")
|
|
228
|
+
return _matches_std_fs_pattern(parts) or _matches_short_fs_pattern(parts)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _matches_std_fs_pattern(parts: list[str]) -> bool:
|
|
232
|
+
"""Check for fully-qualified std::fs::function pattern.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
parts: Path components split by ::
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
True if matches std::fs::function_name
|
|
239
|
+
"""
|
|
240
|
+
if len(parts) < 3:
|
|
241
|
+
return False
|
|
242
|
+
return parts[0] == "std" and parts[1] == "fs" and parts[2] in _BLOCKING_FS_FUNCTIONS
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _matches_short_fs_pattern(parts: list[str]) -> bool:
|
|
246
|
+
"""Check for short fs::function pattern.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
parts: Path components split by ::
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
True if matches fs::function_name
|
|
253
|
+
"""
|
|
254
|
+
if len(parts) < 2:
|
|
255
|
+
return False
|
|
256
|
+
return parts[0] == "fs" and parts[1] in _BLOCKING_FS_FUNCTIONS
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _is_blocking_sleep(path: str) -> bool:
|
|
260
|
+
"""Check if path matches std::thread::sleep.
|
|
261
|
+
|
|
262
|
+
Matches both std::thread::sleep and thread::sleep.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
path: Call path to check
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
True if path is a blocking sleep call
|
|
269
|
+
"""
|
|
270
|
+
parts = path.split("::")
|
|
271
|
+
return _matches_std_sleep_pattern(parts) or _matches_short_sleep_pattern(parts)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _matches_std_sleep_pattern(parts: list[str]) -> bool:
|
|
275
|
+
"""Check for fully-qualified std::thread::sleep pattern.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
parts: Path components split by ::
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
True if matches std::thread::sleep
|
|
282
|
+
"""
|
|
283
|
+
if len(parts) < 3:
|
|
284
|
+
return False
|
|
285
|
+
return parts[0] == "std" and parts[1] == "thread" and parts[2] == "sleep"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _matches_short_sleep_pattern(parts: list[str]) -> bool:
|
|
289
|
+
"""Check for short thread::sleep pattern.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
parts: Path components split by ::
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
True if matches thread::sleep
|
|
296
|
+
"""
|
|
297
|
+
if len(parts) < 2:
|
|
298
|
+
return False
|
|
299
|
+
return parts[0] == "thread" and parts[1] == "sleep"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _is_blocking_net(path: str) -> bool:
|
|
303
|
+
"""Check if path matches a blocking network operation.
|
|
304
|
+
|
|
305
|
+
Matches both fully-qualified (std::net::TcpStream::connect) and
|
|
306
|
+
short paths (net::TcpStream::connect, TcpStream::connect).
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
path: Call path to check
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
True if path is a blocking net operation
|
|
313
|
+
"""
|
|
314
|
+
parts = path.split("::")
|
|
315
|
+
return _matches_std_net_pattern(parts) or _matches_short_net_pattern(parts)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _matches_std_net_pattern(parts: list[str]) -> bool:
|
|
319
|
+
"""Check for fully-qualified std::net::Type::method pattern.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
parts: Path components split by ::
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
True if matches std::net::TcpStream/TcpListener/UdpSocket pattern
|
|
326
|
+
"""
|
|
327
|
+
if len(parts) < 3:
|
|
328
|
+
return False
|
|
329
|
+
return parts[0] == "std" and parts[1] == "net" and parts[2] in _BLOCKING_NET_TYPES
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _matches_short_net_pattern(parts: list[str]) -> bool:
|
|
333
|
+
"""Check for short net::Type::method or Type::method pattern.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
parts: Path components split by ::
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
True if matches short net pattern
|
|
340
|
+
"""
|
|
341
|
+
if len(parts) >= 2 and parts[0] == "net" and parts[1] in _BLOCKING_NET_TYPES:
|
|
342
|
+
return True
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# Function names that safely wrap blocking operations for async execution
|
|
347
|
+
_ASYNC_WRAPPER_FUNCTIONS = frozenset(
|
|
348
|
+
{
|
|
349
|
+
"asyncify",
|
|
350
|
+
"spawn_blocking",
|
|
351
|
+
"block_in_place",
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _is_inside_blocking_wrapper(node: Node) -> bool:
|
|
357
|
+
"""Check if a blocking call is wrapped in an async-safe wrapper function.
|
|
358
|
+
|
|
359
|
+
Detects patterns like asyncify(move || std::fs::read(...)) or
|
|
360
|
+
spawn_blocking(move || { std::fs::write(...) }) where blocking calls
|
|
361
|
+
are correctly offloaded to a thread pool.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
node: The blocking call_expression node
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
True if the call is inside a known async wrapper function
|
|
368
|
+
"""
|
|
369
|
+
current: Node | None = node.parent
|
|
370
|
+
while current is not None:
|
|
371
|
+
if _is_wrapper_call(current):
|
|
372
|
+
return True
|
|
373
|
+
current = current.parent
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _is_wrapper_call(node: Node) -> bool:
|
|
378
|
+
"""Check if a node is a call to a known async wrapper function.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
node: Node to check
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
True if node is a call_expression to asyncify/spawn_blocking/block_in_place
|
|
385
|
+
"""
|
|
386
|
+
if node.type != "call_expression":
|
|
387
|
+
return False
|
|
388
|
+
return any(_child_is_wrapper_name(child) for child in node.children)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _child_is_wrapper_name(child: Node) -> bool:
|
|
392
|
+
"""Check if a child node is a wrapper function name.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
child: Child node of a call_expression
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
True if the child is an identifier or scoped_identifier matching a wrapper name
|
|
399
|
+
"""
|
|
400
|
+
if child.type == "identifier":
|
|
401
|
+
return _node_text_matches_wrapper(child)
|
|
402
|
+
if child.type == "scoped_identifier":
|
|
403
|
+
return _scoped_name_matches_wrapper(child)
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _node_text_matches_wrapper(node: Node) -> bool:
|
|
408
|
+
"""Check if a node's text matches a wrapper function name."""
|
|
409
|
+
text = node.text
|
|
410
|
+
return text is not None and text.decode() in _ASYNC_WRAPPER_FUNCTIONS
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _scoped_name_matches_wrapper(node: Node) -> bool:
|
|
414
|
+
"""Check if a scoped identifier's final segment matches a wrapper function name."""
|
|
415
|
+
text = node.text
|
|
416
|
+
if text is None:
|
|
417
|
+
return False
|
|
418
|
+
func_name = text.decode().split("::")[-1]
|
|
419
|
+
return func_name in _ASYNC_WRAPPER_FUNCTIONS
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Build Violation objects for Rust blocking-in-async patterns
|
|
3
|
+
|
|
4
|
+
Scope: Creates violations with actionable suggestions for fs-in-async, sleep-in-async,
|
|
5
|
+
and net-in-async patterns
|
|
6
|
+
|
|
7
|
+
Overview: Provides module-level functions that create Violation objects for detected
|
|
8
|
+
blocking operations inside async functions in Rust code. Each violation includes the
|
|
9
|
+
rule ID, location, descriptive message explaining the concurrency impact, and a
|
|
10
|
+
suggestion for async-compatible alternatives such as tokio::fs, tokio::time::sleep,
|
|
11
|
+
or tokio::net equivalents.
|
|
12
|
+
|
|
13
|
+
Dependencies: src.core.types for Violation dataclass
|
|
14
|
+
|
|
15
|
+
Exports: build_fs_in_async_violation, build_sleep_in_async_violation, build_net_in_async_violation
|
|
16
|
+
|
|
17
|
+
Interfaces: Module functions taking file_path, line, column, context and returning Violation
|
|
18
|
+
|
|
19
|
+
Implementation: Factory functions for each blocking-in-async pattern with pattern-specific suggestions
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from src.core.types import Violation
|
|
23
|
+
|
|
24
|
+
_FS_IN_ASYNC_SUGGESTION = (
|
|
25
|
+
"Use tokio::fs equivalents (e.g., tokio::fs::read_to_string) for async-compatible "
|
|
26
|
+
"file I/O operations. Blocking std::fs calls in async functions can cause thread "
|
|
27
|
+
"starvation and deadlocks."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_SLEEP_IN_ASYNC_SUGGESTION = (
|
|
31
|
+
"Use tokio::time::sleep instead of std::thread::sleep in async functions. "
|
|
32
|
+
"Blocking the thread with std::thread::sleep prevents the async runtime from "
|
|
33
|
+
"processing other tasks on the same thread."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_NET_IN_ASYNC_SUGGESTION = (
|
|
37
|
+
"Use tokio::net equivalents (e.g., tokio::net::TcpStream) for async-compatible "
|
|
38
|
+
"networking. Blocking std::net calls in async functions can cause thread starvation "
|
|
39
|
+
"and deadlocks in the async runtime."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_fs_in_async_violation(
|
|
44
|
+
file_path: str,
|
|
45
|
+
line: int,
|
|
46
|
+
column: int,
|
|
47
|
+
context: str,
|
|
48
|
+
) -> Violation:
|
|
49
|
+
"""Build a violation for std::fs operation inside an async function."""
|
|
50
|
+
message = f"Blocking std::fs operation inside async function: {context}"
|
|
51
|
+
|
|
52
|
+
return Violation(
|
|
53
|
+
rule_id="blocking-async.fs-in-async",
|
|
54
|
+
file_path=file_path,
|
|
55
|
+
line=line,
|
|
56
|
+
column=column,
|
|
57
|
+
message=message,
|
|
58
|
+
suggestion=_FS_IN_ASYNC_SUGGESTION,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_sleep_in_async_violation(
|
|
63
|
+
file_path: str,
|
|
64
|
+
line: int,
|
|
65
|
+
column: int,
|
|
66
|
+
context: str,
|
|
67
|
+
) -> Violation:
|
|
68
|
+
"""Build a violation for std::thread::sleep inside an async function."""
|
|
69
|
+
message = f"Blocking std::thread::sleep inside async function: {context}"
|
|
70
|
+
|
|
71
|
+
return Violation(
|
|
72
|
+
rule_id="blocking-async.sleep-in-async",
|
|
73
|
+
file_path=file_path,
|
|
74
|
+
line=line,
|
|
75
|
+
column=column,
|
|
76
|
+
message=message,
|
|
77
|
+
suggestion=_SLEEP_IN_ASYNC_SUGGESTION,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_net_in_async_violation(
|
|
82
|
+
file_path: str,
|
|
83
|
+
line: int,
|
|
84
|
+
column: int,
|
|
85
|
+
context: str,
|
|
86
|
+
) -> Violation:
|
|
87
|
+
"""Build a violation for blocking std::net operation inside an async function."""
|
|
88
|
+
message = f"Blocking std::net operation inside async function: {context}"
|
|
89
|
+
|
|
90
|
+
return Violation(
|
|
91
|
+
rule_id="blocking-async.net-in-async",
|
|
92
|
+
file_path=file_path,
|
|
93
|
+
line=line,
|
|
94
|
+
column=column,
|
|
95
|
+
message=message,
|
|
96
|
+
suggestion=_NET_IN_ASYNC_SUGGESTION,
|
|
97
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Rust clone abuse detector package exports
|
|
3
|
+
|
|
4
|
+
Scope: Detect .clone() abuse patterns in Rust code and suggest safer alternatives
|
|
5
|
+
|
|
6
|
+
Overview: Package providing clone abuse detection for Rust code. Identifies .clone() calls
|
|
7
|
+
in loop bodies, chained .clone().clone() calls, and unnecessary clones where the source
|
|
8
|
+
is not used after cloning. Suggests safer alternatives including borrowing, Rc/Arc for
|
|
9
|
+
shared ownership, and Cow for clone-on-write patterns. Supports configuration for allowing
|
|
10
|
+
calls in test code, toggling individual pattern detection, and ignoring specific directories.
|
|
11
|
+
Uses tree-sitter for accurate AST-based detection.
|
|
12
|
+
|
|
13
|
+
Dependencies: tree-sitter-rust (optional) for AST parsing, src.core for base classes
|
|
14
|
+
|
|
15
|
+
Exports: CloneAbuseConfig, CloneAbuseRule, RustCloneAnalyzer, CloneCall
|
|
16
|
+
|
|
17
|
+
Interfaces: CloneAbuseConfig.from_dict() for YAML configuration loading
|
|
18
|
+
|
|
19
|
+
Implementation: Tree-sitter AST-based pattern detection with configurable filtering
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .config import CloneAbuseConfig
|
|
23
|
+
from .linter import CloneAbuseRule
|
|
24
|
+
from .rust_analyzer import CloneCall, RustCloneAnalyzer
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"CloneAbuseConfig",
|
|
28
|
+
"CloneAbuseRule",
|
|
29
|
+
"RustCloneAnalyzer",
|
|
30
|
+
"CloneCall",
|
|
31
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration dataclass for Rust clone abuse detector
|
|
3
|
+
|
|
4
|
+
Scope: Pattern toggles, ignore patterns, and configuration for clone abuse detection
|
|
5
|
+
|
|
6
|
+
Overview: Provides CloneAbuseConfig dataclass with toggles for controlling detection of
|
|
7
|
+
.clone() abuse patterns in Rust code. Supports toggling detection of clone-in-loop,
|
|
8
|
+
clone-chain, and unnecessary-clone patterns independently. Includes configuration for
|
|
9
|
+
allowing calls in test code, ignoring example and benchmark directories. Configuration
|
|
10
|
+
loads from YAML with sensible defaults via from_dict() class method.
|
|
11
|
+
|
|
12
|
+
Dependencies: dataclasses, typing
|
|
13
|
+
|
|
14
|
+
Exports: CloneAbuseConfig
|
|
15
|
+
|
|
16
|
+
Interfaces: CloneAbuseConfig.from_dict() for YAML configuration loading
|
|
17
|
+
|
|
18
|
+
Implementation: Dataclass with factory defaults and conservative default settings
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CloneAbuseConfig:
|
|
27
|
+
"""Configuration for clone abuse detection."""
|
|
28
|
+
|
|
29
|
+
enabled: bool = True
|
|
30
|
+
|
|
31
|
+
# Allow .clone() in test functions and #[cfg(test)] modules
|
|
32
|
+
allow_in_tests: bool = True
|
|
33
|
+
|
|
34
|
+
# Toggle detection of .clone() inside loop bodies
|
|
35
|
+
detect_clone_in_loop: bool = True
|
|
36
|
+
|
|
37
|
+
# Toggle detection of chained .clone().clone() calls
|
|
38
|
+
detect_clone_chain: bool = True
|
|
39
|
+
|
|
40
|
+
# Toggle detection of unnecessary clones (clone before move)
|
|
41
|
+
detect_unnecessary_clone: bool = True
|
|
42
|
+
|
|
43
|
+
# File path patterns to ignore (e.g., examples/, benches/)
|
|
44
|
+
ignore: list[str] = field(default_factory=lambda: ["examples/", "benches/", "tests/"])
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "CloneAbuseConfig":
|
|
48
|
+
"""Load configuration from dictionary.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
config: Configuration dictionary from YAML
|
|
52
|
+
language: Language parameter (reserved for future use)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Configured CloneAbuseConfig instance
|
|
56
|
+
"""
|
|
57
|
+
_ = language
|
|
58
|
+
return cls(
|
|
59
|
+
enabled=config.get("enabled", True),
|
|
60
|
+
allow_in_tests=config.get("allow_in_tests", True),
|
|
61
|
+
detect_clone_in_loop=config.get("detect_clone_in_loop", True),
|
|
62
|
+
detect_clone_chain=config.get("detect_clone_chain", True),
|
|
63
|
+
detect_unnecessary_clone=config.get("detect_unnecessary_clone", True),
|
|
64
|
+
ignore=config.get("ignore", ["examples/", "benches/", "tests/"]),
|
|
65
|
+
)
|