Skip to content

feat(lint): add UnsafeTypecast lint #11046

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Aug 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e8aafe0
bet
TropicalDog17 Jul 19, 2025
0455b31
Merge branch 'master' into feat/unsafe-typecast-lint
TropicalDog17 Jul 19, 2025
befa78a
add more tests, emit fix
TropicalDog17 Jul 23, 2025
34e6a68
Merge branch 'feat/unsafe-typecast-lint' of github.com:TropicalDog17/…
TropicalDog17 Jul 23, 2025
45f5bda
remove unused deps
TropicalDog17 Jul 23, 2025
dff52fa
test: remove compiler error test
TropicalDog17 Jul 23, 2025
2768187
Merge branch 'master' into feat/unsafe-typecast-lint
TropicalDog17 Jul 23, 2025
47e80e4
lint
TropicalDog17 Jul 23, 2025
3614115
refactor
TropicalDog17 Jul 24, 2025
01210d8
Merge branch 'feat/unsafe-typecast-lint' of github.com:TropicalDog17/…
TropicalDog17 Jul 24, 2025
3d4f355
Merge branch 'master' into feat/unsafe-typecast-lint
0xrusowsky Jul 25, 2025
e7febc7
Update crates/lint/src/sol/med/unsafe_typecast.rs
TropicalDog17 Jul 28, 2025
7b541f9
refactor
TropicalDog17 Jul 29, 2025
a541299
refactor
TropicalDog17 Jul 29, 2025
c140190
Merge branch 'master' into feat/unsafe-typecast-lint
TropicalDog17 Jul 29, 2025
403792d
Merge branch 'master' into feat/unsafe-typecast-lint
0xrusowsky Jul 29, 2025
df675c8
Update crates/lint/src/sol/med/unsafe_typecast.rs
TropicalDog17 Jul 29, 2025
8816572
bet
TropicalDog17 Jul 29, 2025
bcf6760
Merge branch 'feat/unsafe-typecast-lint' of github.com:TropicalDog17/…
TropicalDog17 Jul 29, 2025
4883cba
fix: bless files + nits
0xrusowsky Jul 29, 2025
f20127b
fix: infer_source_type for string literals
0xrusowsky Jul 30, 2025
30ee511
style: standardize imports
0xrusowsky Jul 30, 2025
84f8f1b
nit: improve lint msg
0xrusowsky Jul 30, 2025
6fc6866
fix: resolve call type to properly solve cast chains
0xrusowsky Jul 31, 2025
114b609
Merge branch 'master' into feat/unsafe-typecast-lint
0xrusowsky Jul 31, 2025
230e762
fix false positive
TropicalDog17 Aug 10, 2025
1723106
lint
TropicalDog17 Aug 11, 2025
1a6b863
Merge branch 'master' into feat/unsafe-typecast-lint
0xrusowsky Aug 14, 2025
3c31934
fix: account for all types found rather than being limited to just one
0xrusowsky Aug 14, 2025
b32d9cf
Merge branch 'master' into feat/unsafe-typecast-lint
0xrusowsky Aug 14, 2025
8769762
use iter
TropicalDog17 Aug 14, 2025
4fb229a
Merge branch 'master' into feat/unsafe-typecast-lint
0xrusowsky Aug 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion crates/lint/src/sol/med/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@ use crate::sol::{EarlyLintPass, LateLintPass, SolLint};
mod div_mul;
use div_mul::DIVIDE_BEFORE_MULTIPLY;

register_lints!((DivideBeforeMultiply, early, (DIVIDE_BEFORE_MULTIPLY)));
mod unsafe_typecast;
use unsafe_typecast::UNSAFE_TYPECAST;

register_lints!(
(DivideBeforeMultiply, early, (DIVIDE_BEFORE_MULTIPLY)),
(UnsafeTypecast, late, (UNSAFE_TYPECAST))
);
174 changes: 174 additions & 0 deletions crates/lint/src/sol/med/unsafe_typecast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
use super::UnsafeTypecast;
use crate::{
linter::{LateLintPass, LintContext, Snippet},
sol::{Severity, SolLint},
};
use solar_ast::{LitKind, StrKind};
use solar_sema::hir::{self, ElementaryType, ExprKind, ItemId, Res, TypeKind};

declare_forge_lint!(
UNSAFE_TYPECAST,
Severity::Med,
"unsafe-typecast",
"typecasts that can truncate values should be checked"
);

impl<'hir> LateLintPass<'hir> for UnsafeTypecast {
fn check_expr(
&mut self,
ctx: &LintContext<'_>,
hir: &'hir hir::Hir<'hir>,
expr: &'hir hir::Expr<'hir>,
) {
// Check for type cast expressions: Type(value)
if let ExprKind::Call(call, args, _) = &expr.kind
&& let ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ty), .. }) = &call.kind
&& args.len() == 1
&& let Some(call_arg) = args.exprs().next()
&& is_unsafe_typecast_hir(hir, call_arg, ty)
{
ctx.emit_with_fix(
&UNSAFE_TYPECAST,
expr.span,
Snippet::Block {
desc: Some("Consider disabling this lint if you're certain the cast is safe:"),
code: format!(
"// casting to '{abi_ty}' is safe because [explain why]\n// forge-lint: disable-next-line(unsafe-typecast)",
abi_ty = ty.to_abi_str()
)
}
);
}
}
}

/// Determines if a typecast is potentially unsafe (could lose data or precision).
fn is_unsafe_typecast_hir(
hir: &hir::Hir<'_>,
source_expr: &hir::Expr<'_>,
target_type: &hir::ElementaryType,
) -> bool {
let mut source_types = Vec::<ElementaryType>::new();
infer_source_types(Some(&mut source_types), hir, source_expr);

if source_types.is_empty() {
return false;
};

source_types.iter().any(|source_ty| is_unsafe_elementary_typecast(source_ty, target_type))
}

/// Infers the elementary source type(s) of an expression.
///
/// This function traverses an expression tree to find the original "source" types.
/// For cast chains, it returns the ultimate source type, not intermediate cast results.
/// For binary operations, it collects types from both sides into the `output` vector.
///
/// # Returns
/// An `Option<ElementaryType>` containing the inferred type of the expression if it can be
/// resolved to a single source (like variables, literals, or unary expressions).
/// Returns `None` for expressions complex expressions (like binary operations).
fn infer_source_types(
mut output: Option<&mut Vec<ElementaryType>>,
hir: &hir::Hir<'_>,
expr: &hir::Expr<'_>,
) -> Option<ElementaryType> {
let mut track = |ty: ElementaryType| -> Option<ElementaryType> {
if let Some(output) = output.as_mut() {
output.push(ty);
}
Some(ty)
};

match &expr.kind {
// A type cast call: `Type(val)`
ExprKind::Call(call_expr, args, ..) => {
// Check if the called expression is a type, which indicates a cast.
if let ExprKind::Type(hir::Type { kind: TypeKind::Elementary(..), .. }) =
&call_expr.kind
&& let Some(inner) = args.exprs().next()
{
// Recurse to find the original (inner-most) source type.
return infer_source_types(output, hir, inner);
}
None
}

// Identifiers (variables)
ExprKind::Ident(resolutions) => {
if let Some(Res::Item(ItemId::Variable(var_id))) = resolutions.first() {
let variable = hir.variable(*var_id);
if let TypeKind::Elementary(elem_type) = &variable.ty.kind {
return track(*elem_type);
}
}
None
}

// Handle literal values
ExprKind::Lit(hir::Lit { kind, .. }) => match kind {
LitKind::Str(StrKind::Hex, ..) => track(ElementaryType::Bytes),
LitKind::Str(..) => track(ElementaryType::String),
LitKind::Address(_) => track(ElementaryType::Address(false)),
LitKind::Bool(_) => track(ElementaryType::Bool),
// Unnecessary to check numbers as assigning literal values that cannot fit into a type
// throws a compiler error. Reference: <https://solang.readthedocs.io/en/latest/language/types.html>
_ => None,
},

// Unary operations: Recurse to find the source type of the inner expression.
ExprKind::Unary(_, inner_expr) => infer_source_types(output, hir, inner_expr),

// Binary operations
ExprKind::Binary(lhs, _, rhs) => {
if let Some(mut output) = output {
// Recurse on both sides to find and collect all source types.
infer_source_types(Some(&mut output), hir, lhs);
infer_source_types(Some(&mut output), hir, rhs);
}
None
}

// Complex expressions are not evaluated
_ => None,
}
}

/// Checks if a type cast from source_type to target_type is unsafe.
fn is_unsafe_elementary_typecast(
source_type: &ElementaryType,
target_type: &ElementaryType,
) -> bool {
match (source_type, target_type) {
// Numeric downcasts (smaller target size)
(ElementaryType::UInt(source_size), ElementaryType::UInt(target_size))
| (ElementaryType::Int(source_size), ElementaryType::Int(target_size)) => {
source_size.bits() > target_size.bits()
}

// Signed to unsigned conversion (potential loss of sign)
(ElementaryType::Int(_), ElementaryType::UInt(_)) => true,

// Unsigned to signed conversion with same or smaller size
(ElementaryType::UInt(source_size), ElementaryType::Int(target_size)) => {
source_size.bits() >= target_size.bits()
}

// Fixed bytes to smaller fixed bytes
(ElementaryType::FixedBytes(source_size), ElementaryType::FixedBytes(target_size)) => {
source_size.bytes() > target_size.bytes()
}

// Dynamic bytes to fixed bytes (potential truncation)
(ElementaryType::Bytes, ElementaryType::FixedBytes(_))
| (ElementaryType::String, ElementaryType::FixedBytes(_)) => true,

// Address to smaller uint (truncation) - address is 160 bits
(ElementaryType::Address(_), ElementaryType::UInt(target_size)) => target_size.bits() < 160,

// Address to int (sign issues)
(ElementaryType::Address(_), ElementaryType::Int(_)) => true,

_ => false,
}
}
Loading
Loading