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.
Files changed (52) hide show
  1. src/cli/config.py +4 -12
  2. src/cli/linters/__init__.py +13 -3
  3. src/cli/linters/code_patterns.py +42 -38
  4. src/cli/linters/code_smells.py +8 -17
  5. src/cli/linters/documentation.py +3 -6
  6. src/cli/linters/performance.py +4 -10
  7. src/cli/linters/rust.py +177 -0
  8. src/cli/linters/shared.py +2 -7
  9. src/cli/linters/structure.py +4 -11
  10. src/cli/linters/structure_quality.py +4 -11
  11. src/cli/main.py +9 -12
  12. src/cli/utils.py +7 -16
  13. src/core/__init__.py +14 -0
  14. src/core/base.py +30 -0
  15. src/core/constants.py +1 -0
  16. src/core/linter_utils.py +42 -1
  17. src/core/rule_aliases.py +84 -0
  18. src/linter_config/rule_matcher.py +53 -8
  19. src/linters/blocking_async/__init__.py +31 -0
  20. src/linters/blocking_async/config.py +67 -0
  21. src/linters/blocking_async/linter.py +183 -0
  22. src/linters/blocking_async/rust_analyzer.py +419 -0
  23. src/linters/blocking_async/violation_builder.py +97 -0
  24. src/linters/clone_abuse/__init__.py +31 -0
  25. src/linters/clone_abuse/config.py +65 -0
  26. src/linters/clone_abuse/linter.py +183 -0
  27. src/linters/clone_abuse/rust_analyzer.py +356 -0
  28. src/linters/clone_abuse/violation_builder.py +94 -0
  29. src/linters/magic_numbers/linter.py +92 -0
  30. src/linters/magic_numbers/rust_analyzer.py +148 -0
  31. src/linters/magic_numbers/violation_builder.py +31 -0
  32. src/linters/nesting/linter.py +50 -0
  33. src/linters/nesting/rust_analyzer.py +118 -0
  34. src/linters/nesting/violation_builder.py +32 -0
  35. src/linters/print_statements/__init__.py +23 -11
  36. src/linters/print_statements/conditional_verbose_analyzer.py +200 -0
  37. src/linters/print_statements/conditional_verbose_rule.py +254 -0
  38. src/linters/print_statements/linter.py +2 -2
  39. src/linters/srp/class_analyzer.py +49 -0
  40. src/linters/srp/linter.py +22 -0
  41. src/linters/srp/rust_analyzer.py +206 -0
  42. src/linters/unwrap_abuse/__init__.py +30 -0
  43. src/linters/unwrap_abuse/config.py +59 -0
  44. src/linters/unwrap_abuse/linter.py +166 -0
  45. src/linters/unwrap_abuse/rust_analyzer.py +118 -0
  46. src/linters/unwrap_abuse/violation_builder.py +89 -0
  47. src/templates/thailint_config_template.yaml +88 -0
  48. {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/METADATA +7 -3
  49. {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/RECORD +52 -30
  50. {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/WHEEL +0 -0
  51. {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/entry_points.txt +0 -0
  52. {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,183 @@
1
+ """
2
+ Purpose: Main linter rule for detecting blocking operations in async Rust code
3
+
4
+ Scope: Entry point for blocking-in-async detection implementing BaseLintRule interface
5
+
6
+ Overview: Provides BlockingAsyncRule class that implements the BaseLintRule interface for
7
+ detecting blocking API calls inside async functions in Rust code. Validates that files
8
+ are Rust with content, loads configuration, checks ignored paths, and delegates analysis
9
+ to RustBlockingAsyncAnalyzer. Filters detected calls based on configuration (allow_in_tests,
10
+ pattern toggles, ignored paths) and converts remaining calls to Violation objects via the
11
+ violation builder. Supports disabling via configuration.
12
+
13
+ Dependencies: BaseLintRule, RustBlockingAsyncAnalyzer, BlockingAsyncConfig, violation_builder
14
+
15
+ Exports: BlockingAsyncRule
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 BlockingAsyncConfig
32
+ from .rust_analyzer import BlockingCall, RustBlockingAsyncAnalyzer
33
+ from .violation_builder import (
34
+ build_fs_in_async_violation,
35
+ build_net_in_async_violation,
36
+ build_sleep_in_async_violation,
37
+ )
38
+
39
+ _PATTERN_BUILDERS = {
40
+ "fs-in-async": build_fs_in_async_violation,
41
+ "sleep-in-async": build_sleep_in_async_violation,
42
+ "net-in-async": build_net_in_async_violation,
43
+ }
44
+
45
+ _PATTERN_CONFIG_KEYS = {
46
+ "fs-in-async": "detect_fs_in_async",
47
+ "sleep-in-async": "detect_sleep_in_async",
48
+ "net-in-async": "detect_net_in_async",
49
+ }
50
+
51
+
52
+ class BlockingAsyncRule(BaseLintRule):
53
+ """Detects blocking operations inside async functions in Rust code."""
54
+
55
+ def __init__(self, config: BlockingAsyncConfig | None = None) -> None:
56
+ """Initialize blocking-in-async rule.
57
+
58
+ Args:
59
+ config: Optional configuration override for testing
60
+ """
61
+ self._config_override = config
62
+ self._analyzer = RustBlockingAsyncAnalyzer()
63
+
64
+ @property
65
+ def rule_id(self) -> str:
66
+ """Unique identifier for this rule."""
67
+ return "blocking-async"
68
+
69
+ @property
70
+ def rule_name(self) -> str:
71
+ """Human-readable name for this rule."""
72
+ return "Rust Blocking in Async"
73
+
74
+ @property
75
+ def description(self) -> str:
76
+ """Description of what this rule checks."""
77
+ return (
78
+ "Detects blocking operations inside async functions in Rust code including "
79
+ "std::fs I/O, std::thread::sleep, and blocking std::net calls. Suggests "
80
+ "async-compatible alternatives like tokio::fs, tokio::time::sleep, and tokio::net."
81
+ )
82
+
83
+ def check(self, context: BaseLintContext) -> list[Violation]:
84
+ """Check for blocking-in-async 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_blocking_calls(context.file_content or "")
98
+ return self._build_violations(calls, config, file_path)
99
+
100
+ def _should_analyze(self, context: BaseLintContext, config: BlockingAsyncConfig) -> bool:
101
+ """Determine if the file should be analyzed.
102
+
103
+ Args:
104
+ context: Lint context to check
105
+ config: Blocking-in-async 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) -> BlockingAsyncConfig:
119
+ """Load configuration from override or context metadata.
120
+
121
+ Args:
122
+ context: Lint context with metadata
123
+
124
+ Returns:
125
+ BlockingAsyncConfig instance
126
+ """
127
+ if self._config_override is not None:
128
+ return self._config_override
129
+ return load_linter_config(context, "blocking-async", BlockingAsyncConfig)
130
+
131
+ def _build_violations(
132
+ self,
133
+ calls: list[BlockingCall],
134
+ config: BlockingAsyncConfig,
135
+ file_path: str,
136
+ ) -> list[Violation]:
137
+ """Convert blocking calls to violations with config filtering.
138
+
139
+ Args:
140
+ calls: Detected blocking 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: BlockingCall, config: BlockingAsyncConfig) -> bool:
155
+ """Determine if a blocking call should be skipped based on config.
156
+
157
+ Args:
158
+ call: Detected blocking 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: BlockingCall, file_path: str) -> Violation:
173
+ """Build a violation for a specific blocking call.
174
+
175
+ Args:
176
+ call: Detected blocking 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_fs_in_async_violation)
183
+ return builder(file_path, call.line, call.column, call.context)
@@ -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
+ )