thailint 0.16.0__py3-none-any.whl → 0.17.1__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.1.dist-info}/METADATA +8 -3
- {thailint-0.16.0.dist-info → thailint-0.17.1.dist-info}/RECORD +35 -16
- {thailint-0.16.0.dist-info → thailint-0.17.1.dist-info}/WHEEL +0 -0
- {thailint-0.16.0.dist-info → thailint-0.17.1.dist-info}/entry_points.txt +0 -0
- {thailint-0.16.0.dist-info → thailint-0.17.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Rust AST analyzer for detecting SRP violations in Rust structs
|
|
3
|
+
|
|
4
|
+
Scope: RustSRPAnalyzer class for analyzing Rust struct + impl block patterns using tree-sitter
|
|
5
|
+
|
|
6
|
+
Overview: Implements Rust-specific SRP analysis using tree-sitter parser. Extends
|
|
7
|
+
RustBaseAnalyzer to reuse common tree-sitter initialization and traversal patterns.
|
|
8
|
+
Walks the AST to find all struct declarations and impl blocks. Matches impl blocks
|
|
9
|
+
to their corresponding structs by type identifier name. Counts methods across all
|
|
10
|
+
impl blocks for a given struct and calculates total lines of code. Collects
|
|
11
|
+
comprehensive metrics including struct name, method count, LOC, keyword presence,
|
|
12
|
+
and location information. In Rust, a struct + its impl blocks is analogous to a
|
|
13
|
+
class in other languages for SRP analysis purposes.
|
|
14
|
+
|
|
15
|
+
Dependencies: RustBaseAnalyzer, SRPConfig
|
|
16
|
+
|
|
17
|
+
Exports: RustSRPAnalyzer class
|
|
18
|
+
|
|
19
|
+
Interfaces: find_all_structs(root_node), analyze_struct(struct_node, impl_blocks, source, config)
|
|
20
|
+
|
|
21
|
+
Implementation: Inherits tree-sitter parsing from base, aggregates methods across impl blocks
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from src.analyzers.rust_base import RustBaseAnalyzer
|
|
27
|
+
|
|
28
|
+
from .config import SRPConfig
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RustSRPAnalyzer(RustBaseAnalyzer):
|
|
32
|
+
"""Analyzes Rust structs and impl blocks for SRP violations."""
|
|
33
|
+
|
|
34
|
+
def find_all_structs(self, root_node: Any) -> list[Any]:
|
|
35
|
+
"""Find all struct declarations in Rust AST.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
root_node: Root tree-sitter node to search
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of all struct_item nodes
|
|
42
|
+
"""
|
|
43
|
+
return self.walk_tree(root_node, "struct_item")
|
|
44
|
+
|
|
45
|
+
def find_all_impl_blocks(self, root_node: Any) -> list[Any]:
|
|
46
|
+
"""Find all impl blocks in Rust AST.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
root_node: Root tree-sitter node to search
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of all impl_item nodes
|
|
53
|
+
"""
|
|
54
|
+
return self.walk_tree(root_node, "impl_item")
|
|
55
|
+
|
|
56
|
+
def get_impl_target_name(self, impl_node: Any) -> str:
|
|
57
|
+
"""Extract the type name that an impl block targets.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
impl_node: An impl_item tree-sitter node
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The type identifier name (e.g., "Foo" from "impl Foo {}")
|
|
64
|
+
"""
|
|
65
|
+
for child in impl_node.children:
|
|
66
|
+
if child.type == "type_identifier":
|
|
67
|
+
return self.extract_node_text(child)
|
|
68
|
+
return ""
|
|
69
|
+
|
|
70
|
+
def count_impl_methods(self, impl_node: Any) -> int:
|
|
71
|
+
"""Count function items (methods) in an impl block.
|
|
72
|
+
|
|
73
|
+
Counts all function_item children that are public (not prefixed with _).
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
impl_node: An impl_item tree-sitter node
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Number of public methods in the impl block
|
|
80
|
+
"""
|
|
81
|
+
declaration_list = self._find_declaration_list(impl_node)
|
|
82
|
+
if declaration_list is None:
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
count = 0
|
|
86
|
+
for child in declaration_list.children:
|
|
87
|
+
if child.type == "function_item" and self._is_countable_method(child):
|
|
88
|
+
count += 1
|
|
89
|
+
return count
|
|
90
|
+
|
|
91
|
+
def analyze_struct(
|
|
92
|
+
self,
|
|
93
|
+
struct_node: Any,
|
|
94
|
+
impl_blocks: list[Any],
|
|
95
|
+
source: str,
|
|
96
|
+
config: SRPConfig,
|
|
97
|
+
) -> dict[str, Any]:
|
|
98
|
+
"""Analyze a Rust struct and its impl blocks for SRP metrics.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
struct_node: Tree-sitter node representing a struct declaration
|
|
102
|
+
impl_blocks: List of impl_item nodes targeting this struct
|
|
103
|
+
source: Full source code of the file
|
|
104
|
+
config: SRP configuration with thresholds and keywords
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Dictionary with struct metrics (class_name, method_count, loc, etc.)
|
|
108
|
+
"""
|
|
109
|
+
struct_name = self._extract_type_name(struct_node)
|
|
110
|
+
method_count = self._count_total_methods(impl_blocks)
|
|
111
|
+
loc = self._calculate_loc(struct_node, impl_blocks, source)
|
|
112
|
+
has_keyword = any(keyword in struct_name for keyword in config.keywords)
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"class_name": struct_name,
|
|
116
|
+
"method_count": method_count,
|
|
117
|
+
"loc": loc,
|
|
118
|
+
"has_keyword": has_keyword,
|
|
119
|
+
"line": struct_node.start_point[0] + 1,
|
|
120
|
+
"column": struct_node.start_point[1],
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
def _count_total_methods(self, impl_blocks: list[Any]) -> int:
|
|
124
|
+
"""Count methods across all impl blocks.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
impl_blocks: List of impl_item nodes for a struct
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Total public method count
|
|
131
|
+
"""
|
|
132
|
+
return sum(self.count_impl_methods(impl_node) for impl_node in impl_blocks)
|
|
133
|
+
|
|
134
|
+
def _calculate_loc(self, struct_node: Any, impl_blocks: list[Any], source: str) -> int:
|
|
135
|
+
"""Calculate lines of code for struct and its impl blocks.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
struct_node: Struct declaration node
|
|
139
|
+
impl_blocks: List of impl blocks for this struct
|
|
140
|
+
source: Full source code
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Total lines of code
|
|
144
|
+
"""
|
|
145
|
+
struct_loc = self._node_loc(struct_node, source)
|
|
146
|
+
impl_loc = sum(self._node_loc(impl_node, source) for impl_node in impl_blocks)
|
|
147
|
+
return struct_loc + impl_loc
|
|
148
|
+
|
|
149
|
+
def _node_loc(self, node: Any, source: str) -> int:
|
|
150
|
+
"""Calculate lines of code for a single node.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
node: Tree-sitter node
|
|
154
|
+
source: Full source code
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Number of non-blank, non-comment lines
|
|
158
|
+
"""
|
|
159
|
+
start_line = node.start_point[0]
|
|
160
|
+
end_line = node.end_point[0]
|
|
161
|
+
lines = source.split("\n")[start_line : end_line + 1]
|
|
162
|
+
return sum(1 for line in lines if line.strip() and not line.strip().startswith("//"))
|
|
163
|
+
|
|
164
|
+
def _find_declaration_list(self, impl_node: Any) -> Any:
|
|
165
|
+
"""Find the declaration_list node in an impl block.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
impl_node: An impl_item tree-sitter node
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The declaration_list child node or None
|
|
172
|
+
"""
|
|
173
|
+
for child in impl_node.children:
|
|
174
|
+
if child.type == "declaration_list":
|
|
175
|
+
return child
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
def _extract_type_name(self, node: Any) -> str:
|
|
179
|
+
"""Extract type name from a struct or impl node.
|
|
180
|
+
|
|
181
|
+
Rust struct declarations use type_identifier instead of identifier.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
node: A struct_item or impl_item tree-sitter node
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
The type name or "anonymous" fallback
|
|
188
|
+
"""
|
|
189
|
+
for child in node.children:
|
|
190
|
+
if child.type == "type_identifier":
|
|
191
|
+
return self.extract_node_text(child)
|
|
192
|
+
return self.extract_identifier_name(node)
|
|
193
|
+
|
|
194
|
+
def _is_countable_method(self, func_node: Any) -> bool:
|
|
195
|
+
"""Check if a function should be counted as a public method.
|
|
196
|
+
|
|
197
|
+
Excludes functions starting with underscore (private convention).
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
func_node: A function_item tree-sitter node
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
True if the function should be counted
|
|
204
|
+
"""
|
|
205
|
+
name = self.extract_identifier_name(func_node)
|
|
206
|
+
return not name.startswith("_")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Rust unwrap abuse detector package exports
|
|
3
|
+
|
|
4
|
+
Scope: Detect .unwrap() and .expect() abuse in Rust code and suggest safer alternatives
|
|
5
|
+
|
|
6
|
+
Overview: Package providing unwrap/expect abuse detection for Rust code. Identifies .unwrap()
|
|
7
|
+
and .expect() calls outside test code that may panic at runtime. Suggests safer alternatives
|
|
8
|
+
including the ? operator, unwrap_or(), unwrap_or_default(), and match/if-let expressions.
|
|
9
|
+
Supports configuration for allowing calls in test code, example files, and benchmark
|
|
10
|
+
directories. Uses tree-sitter for accurate AST-based detection.
|
|
11
|
+
|
|
12
|
+
Dependencies: tree-sitter-rust (optional) for AST parsing, src.core for base classes
|
|
13
|
+
|
|
14
|
+
Exports: UnwrapAbuseConfig, UnwrapAbuseRule, RustUnwrapAnalyzer, UnwrapCall
|
|
15
|
+
|
|
16
|
+
Interfaces: UnwrapAbuseConfig.from_dict() for YAML configuration loading
|
|
17
|
+
|
|
18
|
+
Implementation: Tree-sitter AST-based pattern detection with configurable filtering
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .config import UnwrapAbuseConfig
|
|
22
|
+
from .linter import UnwrapAbuseRule
|
|
23
|
+
from .rust_analyzer import RustUnwrapAnalyzer, UnwrapCall
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"UnwrapAbuseConfig",
|
|
27
|
+
"UnwrapAbuseRule",
|
|
28
|
+
"RustUnwrapAnalyzer",
|
|
29
|
+
"UnwrapCall",
|
|
30
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration dataclass for Rust unwrap abuse detector
|
|
3
|
+
|
|
4
|
+
Scope: Pattern toggles, ignore patterns, and configuration for unwrap/expect detection
|
|
5
|
+
|
|
6
|
+
Overview: Provides UnwrapAbuseConfig dataclass with toggles for controlling detection of
|
|
7
|
+
.unwrap() and .expect() calls in Rust code. Supports configuration for allowing calls
|
|
8
|
+
in test code, example files, and benchmark directories. Includes an option to allow
|
|
9
|
+
.expect() calls (which provide error context) while still flagging bare .unwrap() calls.
|
|
10
|
+
Configuration loads from YAML with sensible defaults via from_dict() class method.
|
|
11
|
+
|
|
12
|
+
Dependencies: dataclasses, typing
|
|
13
|
+
|
|
14
|
+
Exports: UnwrapAbuseConfig
|
|
15
|
+
|
|
16
|
+
Interfaces: UnwrapAbuseConfig.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 UnwrapAbuseConfig:
|
|
27
|
+
"""Configuration for unwrap abuse detection."""
|
|
28
|
+
|
|
29
|
+
enabled: bool = True
|
|
30
|
+
|
|
31
|
+
# Allow .unwrap()/.expect() in test functions and #[cfg(test)] modules
|
|
32
|
+
allow_in_tests: bool = True
|
|
33
|
+
|
|
34
|
+
# Allow .expect() calls (they provide error context unlike bare .unwrap()).
|
|
35
|
+
# Defaults to True because .expect("reason") is the Rust community recommended
|
|
36
|
+
# alternative to bare .unwrap(), providing panic context.
|
|
37
|
+
allow_expect: bool = True
|
|
38
|
+
|
|
39
|
+
# File path patterns to ignore (e.g., examples/, benches/)
|
|
40
|
+
ignore: list[str] = field(default_factory=lambda: ["examples/", "benches/", "tests/"])
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "UnwrapAbuseConfig":
|
|
44
|
+
"""Load configuration from dictionary.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
config: Configuration dictionary from YAML
|
|
48
|
+
language: Language parameter (reserved for future use)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Configured UnwrapAbuseConfig instance
|
|
52
|
+
"""
|
|
53
|
+
_ = language
|
|
54
|
+
return cls(
|
|
55
|
+
enabled=config.get("enabled", True),
|
|
56
|
+
allow_in_tests=config.get("allow_in_tests", True),
|
|
57
|
+
allow_expect=config.get("allow_expect", True),
|
|
58
|
+
ignore=config.get("ignore", ["examples/", "benches/", "tests/"]),
|
|
59
|
+
)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Main linter rule for detecting unwrap/expect abuse in Rust code
|
|
3
|
+
|
|
4
|
+
Scope: Entry point for unwrap abuse detection implementing BaseLintRule interface
|
|
5
|
+
|
|
6
|
+
Overview: Provides UnwrapAbuseRule class that implements the BaseLintRule interface for
|
|
7
|
+
detecting .unwrap() and .expect() abuse in Rust code. Validates that files are Rust
|
|
8
|
+
with content, loads configuration, checks ignored paths, and delegates analysis to
|
|
9
|
+
RustUnwrapAnalyzer. Filters detected calls based on configuration (allow_in_tests,
|
|
10
|
+
allow_expect, ignored paths) and converts remaining calls to Violation objects via
|
|
11
|
+
the violation builder. Supports disabling via configuration.
|
|
12
|
+
|
|
13
|
+
Dependencies: BaseLintRule, RustUnwrapAnalyzer, UnwrapAbuseConfig, violation_builder
|
|
14
|
+
|
|
15
|
+
Exports: UnwrapAbuseRule
|
|
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 UnwrapAbuseConfig
|
|
32
|
+
from .rust_analyzer import RustUnwrapAnalyzer, UnwrapCall
|
|
33
|
+
from .violation_builder import build_expect_violation, build_unwrap_violation
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class UnwrapAbuseRule(BaseLintRule):
|
|
37
|
+
"""Detects unwrap/expect abuse in Rust code."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, config: UnwrapAbuseConfig | None = None) -> None:
|
|
40
|
+
"""Initialize the unwrap abuse rule.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
config: Optional configuration override for testing
|
|
44
|
+
"""
|
|
45
|
+
self._config_override = config
|
|
46
|
+
self._analyzer = RustUnwrapAnalyzer()
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def rule_id(self) -> str:
|
|
50
|
+
"""Unique identifier for this rule."""
|
|
51
|
+
return "unwrap-abuse"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def rule_name(self) -> str:
|
|
55
|
+
"""Human-readable name for this rule."""
|
|
56
|
+
return "Rust Unwrap Abuse"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def description(self) -> str:
|
|
60
|
+
"""Description of what this rule checks."""
|
|
61
|
+
return (
|
|
62
|
+
"Detects .unwrap() and .expect() calls in Rust code that may panic at runtime. "
|
|
63
|
+
"Suggests safer alternatives like the ? operator, unwrap_or(), unwrap_or_default(), "
|
|
64
|
+
"or match/if-let expressions."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
68
|
+
"""Check for unwrap/expect abuse in Rust code.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
context: The lint context containing file information.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of violations found.
|
|
75
|
+
"""
|
|
76
|
+
config = self._get_config(context)
|
|
77
|
+
if not self._should_analyze(context, config):
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
file_path = resolve_file_path(context)
|
|
81
|
+
calls = self._analyzer.find_unwrap_calls(context.file_content or "")
|
|
82
|
+
return self._build_violations(calls, config, file_path)
|
|
83
|
+
|
|
84
|
+
def _should_analyze(self, context: BaseLintContext, config: UnwrapAbuseConfig) -> bool:
|
|
85
|
+
"""Check if context should be analyzed.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
context: Lint context to check
|
|
89
|
+
config: Active configuration
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
True if file is Rust with content and rule is enabled
|
|
93
|
+
"""
|
|
94
|
+
if context.language != "rust":
|
|
95
|
+
return False
|
|
96
|
+
if not has_file_content(context):
|
|
97
|
+
return False
|
|
98
|
+
if not config.enabled:
|
|
99
|
+
return False
|
|
100
|
+
return not is_ignored_path(resolve_file_path(context), config.ignore)
|
|
101
|
+
|
|
102
|
+
def _get_config(self, context: BaseLintContext) -> UnwrapAbuseConfig:
|
|
103
|
+
"""Get configuration, using override if provided.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
context: Lint context for loading config from metadata
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Configuration instance
|
|
110
|
+
"""
|
|
111
|
+
if self._config_override is not None:
|
|
112
|
+
return self._config_override
|
|
113
|
+
return load_linter_config(context, "unwrap-abuse", UnwrapAbuseConfig)
|
|
114
|
+
|
|
115
|
+
def _build_violations(
|
|
116
|
+
self,
|
|
117
|
+
calls: list[UnwrapCall],
|
|
118
|
+
config: UnwrapAbuseConfig,
|
|
119
|
+
file_path: str,
|
|
120
|
+
) -> list[Violation]:
|
|
121
|
+
"""Convert filtered unwrap calls to violations.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
calls: Detected unwrap/expect calls
|
|
125
|
+
config: Active configuration
|
|
126
|
+
file_path: Path to the file being analyzed
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of violations for non-excluded calls
|
|
130
|
+
"""
|
|
131
|
+
return [
|
|
132
|
+
_build_violation_for_call(call, file_path)
|
|
133
|
+
for call in calls
|
|
134
|
+
if not self._should_skip_call(call, config)
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
def _should_skip_call(self, call: UnwrapCall, config: UnwrapAbuseConfig) -> bool:
|
|
138
|
+
"""Determine if a detected call should be skipped based on config.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
call: Detected unwrap/expect call
|
|
142
|
+
config: Active configuration
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if the call should be excluded from violations
|
|
146
|
+
"""
|
|
147
|
+
if call.is_in_test and config.allow_in_tests:
|
|
148
|
+
return True
|
|
149
|
+
if call.method == "expect" and config.allow_expect:
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _build_violation_for_call(call: UnwrapCall, file_path: str) -> Violation:
|
|
155
|
+
"""Build the appropriate violation for a detected call.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
call: Detected unwrap/expect call
|
|
159
|
+
file_path: Path to the file
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Violation object for the call
|
|
163
|
+
"""
|
|
164
|
+
if call.method == "unwrap":
|
|
165
|
+
return build_unwrap_violation(file_path, call.line, call.column, call.context)
|
|
166
|
+
return build_expect_violation(file_path, call.line, call.column, call.context)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Analyzer for detecting unwrap/expect abuse in Rust code using tree-sitter AST
|
|
3
|
+
|
|
4
|
+
Scope: Pattern detection for .unwrap() and .expect() method calls in Rust source files
|
|
5
|
+
|
|
6
|
+
Overview: Provides RustUnwrapAnalyzer that extends RustBaseAnalyzer to detect .unwrap() and
|
|
7
|
+
.expect() method calls in Rust code. Uses tree-sitter AST to find call_expression nodes
|
|
8
|
+
containing field_expression with field_identifier matching "unwrap" or "expect". Determines
|
|
9
|
+
whether each call is inside test code using the base analyzer's is_inside_test() method.
|
|
10
|
+
Returns structured UnwrapCall dataclass instances with location, method name, test context,
|
|
11
|
+
and surrounding code for violation reporting.
|
|
12
|
+
|
|
13
|
+
Dependencies: src.analyzers.rust_base for tree-sitter parsing and traversal
|
|
14
|
+
|
|
15
|
+
Exports: RustUnwrapAnalyzer, UnwrapCall
|
|
16
|
+
|
|
17
|
+
Interfaces: find_unwrap_calls(code: str) -> list[UnwrapCall]
|
|
18
|
+
|
|
19
|
+
Implementation: Recursive AST traversal with field_expression pattern matching for method calls
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
from src.analyzers.rust_base import TREE_SITTER_RUST_AVAILABLE, RustBaseAnalyzer
|
|
25
|
+
from src.core.linter_utils import get_line_context
|
|
26
|
+
|
|
27
|
+
if TREE_SITTER_RUST_AVAILABLE:
|
|
28
|
+
from tree_sitter import Node
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class UnwrapCall:
|
|
33
|
+
"""Represents a detected unwrap/expect call."""
|
|
34
|
+
|
|
35
|
+
line: int
|
|
36
|
+
column: int
|
|
37
|
+
method: str # "unwrap" or "expect"
|
|
38
|
+
is_in_test: bool
|
|
39
|
+
context: str # Surrounding code snippet
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RustUnwrapAnalyzer(RustBaseAnalyzer):
|
|
43
|
+
"""Analyzer for detecting unwrap/expect calls in Rust code."""
|
|
44
|
+
|
|
45
|
+
def find_unwrap_calls(self, code: str) -> list[UnwrapCall]:
|
|
46
|
+
"""Find all unwrap() and expect() calls in code.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
code: Rust source code to analyze
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of detected unwrap/expect calls with location and context
|
|
53
|
+
"""
|
|
54
|
+
if not self.tree_sitter_available:
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
root = self.parse_rust(code)
|
|
58
|
+
if root is None:
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
calls: list[UnwrapCall] = []
|
|
62
|
+
self._find_unwrap_recursive(root, code, calls)
|
|
63
|
+
return calls
|
|
64
|
+
|
|
65
|
+
def _find_unwrap_recursive(self, node: "Node", code: str, calls: list[UnwrapCall]) -> None:
|
|
66
|
+
"""Recursively find unwrap/expect method calls in AST.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
node: Current tree-sitter node to inspect
|
|
70
|
+
code: Original source code for context extraction
|
|
71
|
+
calls: Accumulator list for detected calls
|
|
72
|
+
"""
|
|
73
|
+
if node.type == "call_expression":
|
|
74
|
+
method_name = self._get_method_name(node)
|
|
75
|
+
if method_name in ("unwrap", "expect"):
|
|
76
|
+
calls.append(
|
|
77
|
+
UnwrapCall(
|
|
78
|
+
line=node.start_point[0] + 1,
|
|
79
|
+
column=node.start_point[1],
|
|
80
|
+
method=method_name,
|
|
81
|
+
is_in_test=self.is_inside_test(node),
|
|
82
|
+
context=get_line_context(code, node.start_point[0]),
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
for child in node.children:
|
|
87
|
+
self._find_unwrap_recursive(child, code, calls)
|
|
88
|
+
|
|
89
|
+
def _get_method_name(self, call_node: "Node") -> str:
|
|
90
|
+
"""Extract method name from a call expression.
|
|
91
|
+
|
|
92
|
+
In Rust tree-sitter, method calls have this structure:
|
|
93
|
+
call_expression -> field_expression -> field_identifier
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
call_node: A call_expression node
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Method name string, or empty string if not a method call
|
|
100
|
+
"""
|
|
101
|
+
for child in call_node.children:
|
|
102
|
+
if child.type == "field_expression":
|
|
103
|
+
return self._extract_field_identifier(child)
|
|
104
|
+
return ""
|
|
105
|
+
|
|
106
|
+
def _extract_field_identifier(self, field_expr: "Node") -> str:
|
|
107
|
+
"""Extract the field identifier from a field expression.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
field_expr: A field_expression node
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
The field identifier text (e.g., "unwrap", "expect")
|
|
114
|
+
"""
|
|
115
|
+
for subchild in field_expr.children:
|
|
116
|
+
if subchild.type == "field_identifier":
|
|
117
|
+
return self.extract_node_text(subchild)
|
|
118
|
+
return ""
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Build Violation objects for Rust unwrap/expect abuse patterns
|
|
3
|
+
|
|
4
|
+
Scope: Creates violations with actionable suggestions for unwrap and expect calls
|
|
5
|
+
|
|
6
|
+
Overview: Provides module-level functions that create Violation objects for detected
|
|
7
|
+
.unwrap() and .expect() calls in Rust code. Each violation includes the rule ID,
|
|
8
|
+
location, descriptive message explaining the risk of panicking, and a suggestion
|
|
9
|
+
for safer alternatives such as the ? operator, unwrap_or(), unwrap_or_default(),
|
|
10
|
+
or match/if-let expressions.
|
|
11
|
+
|
|
12
|
+
Dependencies: src.core.types for Violation dataclass
|
|
13
|
+
|
|
14
|
+
Exports: build_unwrap_violation, build_expect_violation
|
|
15
|
+
|
|
16
|
+
Interfaces: Module functions taking file_path, line, column, context and returning Violation
|
|
17
|
+
|
|
18
|
+
Implementation: Factory functions for unwrap and expect violation types with pattern-specific suggestions
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from src.core.types import Violation
|
|
22
|
+
|
|
23
|
+
_UNWRAP_SUGGESTION = (
|
|
24
|
+
"Use the ? operator, .unwrap_or(), .unwrap_or_default(), "
|
|
25
|
+
"or match/if-let for safe error handling."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_EXPECT_SUGGESTION = (
|
|
29
|
+
"Use the ? operator with a descriptive error via .context() or .with_context(), "
|
|
30
|
+
"or use match/if-let for explicit error handling."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_unwrap_violation(
|
|
35
|
+
file_path: str,
|
|
36
|
+
line: int,
|
|
37
|
+
column: int,
|
|
38
|
+
context: str,
|
|
39
|
+
) -> Violation:
|
|
40
|
+
"""Build a violation for .unwrap() call.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
file_path: Path to the file containing the violation
|
|
44
|
+
line: Line number (1-indexed)
|
|
45
|
+
column: Column number (0-indexed)
|
|
46
|
+
context: Code context around the violation
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Violation object with safer alternative suggestion
|
|
50
|
+
"""
|
|
51
|
+
message = f".unwrap() call may panic at runtime: {context}"
|
|
52
|
+
|
|
53
|
+
return Violation(
|
|
54
|
+
rule_id="unwrap-abuse.unwrap-call",
|
|
55
|
+
file_path=file_path,
|
|
56
|
+
line=line,
|
|
57
|
+
column=column,
|
|
58
|
+
message=message,
|
|
59
|
+
suggestion=_UNWRAP_SUGGESTION,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_expect_violation(
|
|
64
|
+
file_path: str,
|
|
65
|
+
line: int,
|
|
66
|
+
column: int,
|
|
67
|
+
context: str,
|
|
68
|
+
) -> Violation:
|
|
69
|
+
"""Build a violation for .expect() call.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
file_path: Path to the file containing the violation
|
|
73
|
+
line: Line number (1-indexed)
|
|
74
|
+
column: Column number (0-indexed)
|
|
75
|
+
context: Code context around the violation
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Violation object with safer alternative suggestion
|
|
79
|
+
"""
|
|
80
|
+
message = f".expect() call may panic at runtime: {context}"
|
|
81
|
+
|
|
82
|
+
return Violation(
|
|
83
|
+
rule_id="unwrap-abuse.expect-call",
|
|
84
|
+
file_path=file_path,
|
|
85
|
+
line=line,
|
|
86
|
+
column=column,
|
|
87
|
+
message=message,
|
|
88
|
+
suggestion=_EXPECT_SUGGESTION,
|
|
89
|
+
)
|