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,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
|
+
)
|