Skip to content

Commit 1186156

Browse files
TropicalDog170xrusowsky0xClandestine
authored
feat(lint): add UnsafeTypecast lint (#11046)
--------- Co-authored-by: 0xrusowsky <[email protected]> Co-authored-by: clandestine.eth <[email protected]> Co-authored-by: 0xrusowsky <[email protected]>
1 parent 3412508 commit 1186156

File tree

4 files changed

+2755
-1
lines changed

4 files changed

+2755
-1
lines changed

crates/lint/src/sol/med/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,10 @@ use crate::sol::{EarlyLintPass, LateLintPass, SolLint};
33
mod div_mul;
44
use div_mul::DIVIDE_BEFORE_MULTIPLY;
55

6-
register_lints!((DivideBeforeMultiply, early, (DIVIDE_BEFORE_MULTIPLY)));
6+
mod unsafe_typecast;
7+
use unsafe_typecast::UNSAFE_TYPECAST;
8+
9+
register_lints!(
10+
(DivideBeforeMultiply, early, (DIVIDE_BEFORE_MULTIPLY)),
11+
(UnsafeTypecast, late, (UNSAFE_TYPECAST))
12+
);
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
use super::UnsafeTypecast;
2+
use crate::{
3+
linter::{LateLintPass, LintContext, Snippet},
4+
sol::{Severity, SolLint},
5+
};
6+
use solar_ast::{LitKind, StrKind};
7+
use solar_sema::hir::{self, ElementaryType, ExprKind, ItemId, Res, TypeKind};
8+
9+
declare_forge_lint!(
10+
UNSAFE_TYPECAST,
11+
Severity::Med,
12+
"unsafe-typecast",
13+
"typecasts that can truncate values should be checked"
14+
);
15+
16+
impl<'hir> LateLintPass<'hir> for UnsafeTypecast {
17+
fn check_expr(
18+
&mut self,
19+
ctx: &LintContext<'_>,
20+
hir: &'hir hir::Hir<'hir>,
21+
expr: &'hir hir::Expr<'hir>,
22+
) {
23+
// Check for type cast expressions: Type(value)
24+
if let ExprKind::Call(call, args, _) = &expr.kind
25+
&& let ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ty), .. }) = &call.kind
26+
&& args.len() == 1
27+
&& let Some(call_arg) = args.exprs().next()
28+
&& is_unsafe_typecast_hir(hir, call_arg, ty)
29+
{
30+
ctx.emit_with_fix(
31+
&UNSAFE_TYPECAST,
32+
expr.span,
33+
Snippet::Block {
34+
desc: Some("Consider disabling this lint if you're certain the cast is safe:"),
35+
code: format!(
36+
"// casting to '{abi_ty}' is safe because [explain why]\n// forge-lint: disable-next-line(unsafe-typecast)",
37+
abi_ty = ty.to_abi_str()
38+
)
39+
}
40+
);
41+
}
42+
}
43+
}
44+
45+
/// Determines if a typecast is potentially unsafe (could lose data or precision).
46+
fn is_unsafe_typecast_hir(
47+
hir: &hir::Hir<'_>,
48+
source_expr: &hir::Expr<'_>,
49+
target_type: &hir::ElementaryType,
50+
) -> bool {
51+
let mut source_types = Vec::<ElementaryType>::new();
52+
infer_source_types(Some(&mut source_types), hir, source_expr);
53+
54+
if source_types.is_empty() {
55+
return false;
56+
};
57+
58+
source_types.iter().any(|source_ty| is_unsafe_elementary_typecast(source_ty, target_type))
59+
}
60+
61+
/// Infers the elementary source type(s) of an expression.
62+
///
63+
/// This function traverses an expression tree to find the original "source" types.
64+
/// For cast chains, it returns the ultimate source type, not intermediate cast results.
65+
/// For binary operations, it collects types from both sides into the `output` vector.
66+
///
67+
/// # Returns
68+
/// An `Option<ElementaryType>` containing the inferred type of the expression if it can be
69+
/// resolved to a single source (like variables, literals, or unary expressions).
70+
/// Returns `None` for expressions complex expressions (like binary operations).
71+
fn infer_source_types(
72+
mut output: Option<&mut Vec<ElementaryType>>,
73+
hir: &hir::Hir<'_>,
74+
expr: &hir::Expr<'_>,
75+
) -> Option<ElementaryType> {
76+
let mut track = |ty: ElementaryType| -> Option<ElementaryType> {
77+
if let Some(output) = output.as_mut() {
78+
output.push(ty);
79+
}
80+
Some(ty)
81+
};
82+
83+
match &expr.kind {
84+
// A type cast call: `Type(val)`
85+
ExprKind::Call(call_expr, args, ..) => {
86+
// Check if the called expression is a type, which indicates a cast.
87+
if let ExprKind::Type(hir::Type { kind: TypeKind::Elementary(..), .. }) =
88+
&call_expr.kind
89+
&& let Some(inner) = args.exprs().next()
90+
{
91+
// Recurse to find the original (inner-most) source type.
92+
return infer_source_types(output, hir, inner);
93+
}
94+
None
95+
}
96+
97+
// Identifiers (variables)
98+
ExprKind::Ident(resolutions) => {
99+
if let Some(Res::Item(ItemId::Variable(var_id))) = resolutions.first() {
100+
let variable = hir.variable(*var_id);
101+
if let TypeKind::Elementary(elem_type) = &variable.ty.kind {
102+
return track(*elem_type);
103+
}
104+
}
105+
None
106+
}
107+
108+
// Handle literal values
109+
ExprKind::Lit(hir::Lit { kind, .. }) => match kind {
110+
LitKind::Str(StrKind::Hex, ..) => track(ElementaryType::Bytes),
111+
LitKind::Str(..) => track(ElementaryType::String),
112+
LitKind::Address(_) => track(ElementaryType::Address(false)),
113+
LitKind::Bool(_) => track(ElementaryType::Bool),
114+
// Unnecessary to check numbers as assigning literal values that cannot fit into a type
115+
// throws a compiler error. Reference: <https://solang.readthedocs.io/en/latest/language/types.html>
116+
_ => None,
117+
},
118+
119+
// Unary operations: Recurse to find the source type of the inner expression.
120+
ExprKind::Unary(_, inner_expr) => infer_source_types(output, hir, inner_expr),
121+
122+
// Binary operations
123+
ExprKind::Binary(lhs, _, rhs) => {
124+
if let Some(mut output) = output {
125+
// Recurse on both sides to find and collect all source types.
126+
infer_source_types(Some(&mut output), hir, lhs);
127+
infer_source_types(Some(&mut output), hir, rhs);
128+
}
129+
None
130+
}
131+
132+
// Complex expressions are not evaluated
133+
_ => None,
134+
}
135+
}
136+
137+
/// Checks if a type cast from source_type to target_type is unsafe.
138+
fn is_unsafe_elementary_typecast(
139+
source_type: &ElementaryType,
140+
target_type: &ElementaryType,
141+
) -> bool {
142+
match (source_type, target_type) {
143+
// Numeric downcasts (smaller target size)
144+
(ElementaryType::UInt(source_size), ElementaryType::UInt(target_size))
145+
| (ElementaryType::Int(source_size), ElementaryType::Int(target_size)) => {
146+
source_size.bits() > target_size.bits()
147+
}
148+
149+
// Signed to unsigned conversion (potential loss of sign)
150+
(ElementaryType::Int(_), ElementaryType::UInt(_)) => true,
151+
152+
// Unsigned to signed conversion with same or smaller size
153+
(ElementaryType::UInt(source_size), ElementaryType::Int(target_size)) => {
154+
source_size.bits() >= target_size.bits()
155+
}
156+
157+
// Fixed bytes to smaller fixed bytes
158+
(ElementaryType::FixedBytes(source_size), ElementaryType::FixedBytes(target_size)) => {
159+
source_size.bytes() > target_size.bytes()
160+
}
161+
162+
// Dynamic bytes to fixed bytes (potential truncation)
163+
(ElementaryType::Bytes, ElementaryType::FixedBytes(_))
164+
| (ElementaryType::String, ElementaryType::FixedBytes(_)) => true,
165+
166+
// Address to smaller uint (truncation) - address is 160 bits
167+
(ElementaryType::Address(_), ElementaryType::UInt(target_size)) => target_size.bits() < 160,
168+
169+
// Address to int (sign issues)
170+
(ElementaryType::Address(_), ElementaryType::Int(_)) => true,
171+
172+
_ => false,
173+
}
174+
}

0 commit comments

Comments
 (0)