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.
Files changed (35) hide show
  1. src/cli/linters/__init__.py +8 -1
  2. src/cli/linters/rust.py +177 -0
  3. src/core/base.py +30 -0
  4. src/core/constants.py +1 -0
  5. src/core/linter_utils.py +42 -1
  6. src/linters/blocking_async/__init__.py +31 -0
  7. src/linters/blocking_async/config.py +67 -0
  8. src/linters/blocking_async/linter.py +183 -0
  9. src/linters/blocking_async/rust_analyzer.py +419 -0
  10. src/linters/blocking_async/violation_builder.py +97 -0
  11. src/linters/clone_abuse/__init__.py +31 -0
  12. src/linters/clone_abuse/config.py +65 -0
  13. src/linters/clone_abuse/linter.py +183 -0
  14. src/linters/clone_abuse/rust_analyzer.py +356 -0
  15. src/linters/clone_abuse/violation_builder.py +94 -0
  16. src/linters/magic_numbers/linter.py +92 -0
  17. src/linters/magic_numbers/rust_analyzer.py +148 -0
  18. src/linters/magic_numbers/violation_builder.py +31 -0
  19. src/linters/nesting/linter.py +50 -0
  20. src/linters/nesting/rust_analyzer.py +118 -0
  21. src/linters/nesting/violation_builder.py +32 -0
  22. src/linters/srp/class_analyzer.py +49 -0
  23. src/linters/srp/linter.py +22 -0
  24. src/linters/srp/rust_analyzer.py +206 -0
  25. src/linters/unwrap_abuse/__init__.py +30 -0
  26. src/linters/unwrap_abuse/config.py +59 -0
  27. src/linters/unwrap_abuse/linter.py +166 -0
  28. src/linters/unwrap_abuse/rust_analyzer.py +118 -0
  29. src/linters/unwrap_abuse/violation_builder.py +89 -0
  30. src/templates/thailint_config_template.yaml +88 -0
  31. {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/METADATA +5 -2
  32. {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/RECORD +35 -16
  33. {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/WHEEL +0 -0
  34. {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/entry_points.txt +0 -0
  35. {thailint-0.16.0.dist-info → thailint-0.17.0.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
+ )