1use super::TypeBasedTautology;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{Severity, SolLint},
5};
6use alloy_primitives::U256;
7use solar::{
8 ast::{BinOpKind, LitKind, UnOpKind},
9 sema::{
10 Gcx,
11 hir::{self, ElementaryType, ExprKind, ItemId, Res, TypeKind, VariableId},
12 },
13};
14
15declare_forge_lint!(
16 TYPE_BASED_TAUTOLOGY,
17 Severity::Med,
18 "type-based-tautology",
19 "condition is always true or false based on the variable's type"
20);
21
22impl<'hir> LateLintPass<'hir> for TypeBasedTautology {
23 fn check_expr(
24 &mut self,
25 ctx: &LintContext,
26 _gcx: Gcx<'hir>,
27 hir: &'hir hir::Hir<'hir>,
28 expr: &'hir hir::Expr<'hir>,
29 ) {
30 let ExprKind::Binary(left, op, right) = &expr.kind else { return };
31
32 if op.kind == BinOpKind::Or
35 && let (Some(left), Some(right)) = (comparison_of(hir, left), comparison_of(hir, right))
36 && is_boundary_composition(&left, &right)
37 {
38 ctx.emit(&TYPE_BASED_TAUTOLOGY, expr.span);
39 return;
40 }
41
42 if !matches!(
44 op.kind,
45 BinOpKind::Lt
46 | BinOpKind::Le
47 | BinOpKind::Gt
48 | BinOpKind::Ge
49 | BinOpKind::Eq
50 | BinOpKind::Ne
51 ) {
52 return;
53 }
54
55 if let Some(elem_ty) = elem_type_of(hir, left)
57 && let Some((val_neg, val_mag)) = lit_value_of(right)
58 && is_tautology(elem_ty, val_neg, val_mag, op.kind)
59 {
60 ctx.emit(&TYPE_BASED_TAUTOLOGY, expr.span);
61 return;
62 }
63
64 if let Some((val_neg, val_mag)) = lit_value_of(left)
66 && let Some(elem_ty) = elem_type_of(hir, right)
67 && is_tautology(elem_ty, val_neg, val_mag, flip(op.kind))
68 {
69 ctx.emit(&TYPE_BASED_TAUTOLOGY, expr.span);
70 }
71 }
72}
73
74#[derive(Clone)]
75struct Comparison {
76 variable: VariableId,
77 cast_path: Vec<ElementaryType>,
78 range: IntegerRange,
79 op: BinOpKind,
80 val_neg: bool,
81 val_mag: U256,
82}
83
84#[derive(Clone, Copy, PartialEq, Eq)]
85struct IntegerRange {
86 lower: (bool, U256),
87 upper: (bool, U256),
88}
89
90fn comparison_of<'hir>(
92 hir: &'hir hir::Hir<'hir>,
93 expr: &'hir hir::Expr<'hir>,
94) -> Option<Comparison> {
95 let ExprKind::Binary(left, op, right) = expr.peel_parens().kind else { return None };
96 if !matches!(
97 op.kind,
98 BinOpKind::Lt
99 | BinOpKind::Le
100 | BinOpKind::Gt
101 | BinOpKind::Ge
102 | BinOpKind::Eq
103 | BinOpKind::Ne
104 ) {
105 return None;
106 }
107
108 if let (Some((variable, cast_path, range)), Some((val_neg, val_mag))) =
109 (comparison_operand_of(hir, left), lit_value_of(right))
110 {
111 return Some(Comparison { variable, cast_path, range, op: op.kind, val_neg, val_mag });
112 }
113
114 if let (Some((val_neg, val_mag)), Some((variable, cast_path, range))) =
115 (lit_value_of(left), comparison_operand_of(hir, right))
116 {
117 return Some(Comparison {
118 variable,
119 cast_path,
120 range,
121 op: flip(op.kind),
122 val_neg,
123 val_mag,
124 });
125 }
126
127 None
128}
129
130fn is_boundary_composition(left: &Comparison, right: &Comparison) -> bool {
132 if left.variable != right.variable
133 || left.cast_path != right.cast_path
134 || left.range != right.range
135 {
136 return false;
137 }
138
139 let lower = left.range.lower;
140 let upper = left.range.upper;
141
142 (matches_comparison(left, BinOpKind::Gt, lower) && is_lower_point(right, lower))
144 || (matches_comparison(right, BinOpKind::Gt, lower) && is_lower_point(left, lower))
145 || (matches_comparison(left, BinOpKind::Lt, upper) && is_upper_point(right, upper))
147 || (matches_comparison(right, BinOpKind::Lt, upper) && is_upper_point(left, upper))
148 || (matches_comparison(left, BinOpKind::Gt, lower)
150 && matches_comparison(right, BinOpKind::Lt, upper))
151 || (matches_comparison(right, BinOpKind::Gt, lower)
152 && matches_comparison(left, BinOpKind::Lt, upper))
153}
154
155fn matches_comparison(comparison: &Comparison, op: BinOpKind, value: (bool, U256)) -> bool {
156 comparison.op == op && comparison.val_neg == value.0 && comparison.val_mag == value.1
157}
158
159fn is_lower_point(comparison: &Comparison, lower: (bool, U256)) -> bool {
160 (comparison.op == BinOpKind::Eq || comparison.op == BinOpKind::Le)
161 && comparison.val_neg == lower.0
162 && comparison.val_mag == lower.1
163}
164
165fn is_upper_point(comparison: &Comparison, upper: (bool, U256)) -> bool {
166 (comparison.op == BinOpKind::Eq || comparison.op == BinOpKind::Ge)
167 && comparison.val_neg == upper.0
168 && comparison.val_mag == upper.1
169}
170
171fn integer_bounds(ty: ElementaryType) -> Option<IntegerRange> {
172 match ty {
173 ElementaryType::UInt(size) => {
174 let bits = size.bits();
175 let upper =
176 if bits == 256 { U256::MAX } else { (U256::from(1u8) << bits) - U256::from(1u8) };
177 Some(IntegerRange { lower: (false, U256::ZERO), upper: (false, upper) })
178 }
179 ElementaryType::Int(size) => {
180 let half = U256::from(1u8) << (size.bits() - 1);
181 Some(IntegerRange { lower: (true, half), upper: (false, half - U256::from(1u8)) })
182 }
183 _ => None,
184 }
185}
186
187const fn flip(op: BinOpKind) -> BinOpKind {
190 match op {
191 BinOpKind::Lt => BinOpKind::Gt,
192 BinOpKind::Le => BinOpKind::Ge,
193 BinOpKind::Gt => BinOpKind::Lt,
194 BinOpKind::Ge => BinOpKind::Le,
195 BinOpKind::Eq | BinOpKind::Ne => op, _ => unreachable!(),
197 }
198}
199
200fn is_tautology(ty: ElementaryType, val_neg: bool, val_mag: U256, op: BinOpKind) -> bool {
206 match ty {
207 ElementaryType::UInt(size) => {
208 let bits = size.bits();
210 let hi =
211 if bits == 256 { U256::MAX } else { (U256::from(1u8) << bits) - U256::from(1u8) };
212 let val_lt_lo = val_neg && val_mag != U256::ZERO; let val_le_lo = val_neg || val_mag == U256::ZERO; let hi_lt_val = !val_neg && val_mag > hi; let hi_le_val = !val_neg && val_mag >= hi; match op {
217 BinOpKind::Gt | BinOpKind::Le => hi_le_val || val_lt_lo,
218 BinOpKind::Ge | BinOpKind::Lt => val_le_lo || hi_lt_val,
219 BinOpKind::Eq | BinOpKind::Ne => hi_lt_val || val_lt_lo,
220 _ => false,
221 }
222 }
223 ElementaryType::Int(size) => {
224 let bits = size.bits();
226 let half = U256::from(1u8) << (bits - 1); let hi = half - U256::from(1u8); let val_lt_lo = val_neg && val_mag > half; let val_le_lo = val_neg && val_mag >= half; let hi_lt_val = !val_neg && val_mag > hi; let hi_le_val = !val_neg && val_mag >= hi; match op {
233 BinOpKind::Gt | BinOpKind::Le => hi_le_val || val_lt_lo,
234 BinOpKind::Ge | BinOpKind::Lt => val_le_lo || hi_lt_val,
235 BinOpKind::Eq | BinOpKind::Ne => hi_lt_val || val_lt_lo,
236 _ => false,
237 }
238 }
239 _ => false,
240 }
241}
242
243fn elem_type_of<'hir>(
245 hir: &'hir hir::Hir<'hir>,
246 expr: &'hir hir::Expr<'hir>,
247) -> Option<ElementaryType> {
248 match &expr.peel_parens().kind {
249 ExprKind::Ident(resolutions) => {
250 if let Some(Res::Item(ItemId::Variable(var_id))) = resolutions.first()
251 && let TypeKind::Elementary(ty) = hir.variable(*var_id).ty.kind
252 {
253 return Some(ty);
254 }
255 None
256 }
257 ExprKind::Call(call_expr, _, _) => {
259 if let ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ty), .. }) =
260 &call_expr.kind
261 {
262 return Some(*ty);
263 }
264 None
265 }
266 _ => None,
267 }
268}
269
270fn comparison_operand_of<'hir>(
277 hir: &'hir hir::Hir<'hir>,
278 expr: &'hir hir::Expr<'hir>,
279) -> Option<(VariableId, Vec<ElementaryType>, IntegerRange)> {
280 match &expr.peel_parens().kind {
281 ExprKind::Ident(resolutions) => {
282 if let Some(Res::Item(ItemId::Variable(variable))) = resolutions.first()
283 && let TypeKind::Elementary(ty) = hir.variable(*variable).ty.kind
284 {
285 return integer_bounds(ty).map(|range| (*variable, Vec::new(), range));
286 }
287 }
288 ExprKind::Call(call_expr, args, _) => {
289 let ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ty), .. }) = &call_expr.kind
290 else {
291 return None;
292 };
293 if !matches!(ty, ElementaryType::Int(_) | ElementaryType::UInt(_)) {
294 return None;
295 }
296
297 let mut exprs = args.exprs();
298 let inner = exprs.next()?;
299 if exprs.next().is_some() {
300 return None;
301 }
302 let (variable, mut cast_path, source_range) = comparison_operand_of(hir, inner)?;
303 let source_type = expression_type(hir, inner)?;
304 let range = effective_range_for_cast(source_type, source_range, *ty)?;
305 if !is_value_preserving_widening(source_type, *ty) {
308 cast_path.push(*ty);
309 }
310 return Some((variable, cast_path, range));
311 }
312 _ => {}
313 }
314 None
315}
316
317fn effective_range_for_cast(
318 source_type: ElementaryType,
319 source_range: IntegerRange,
320 target_type: ElementaryType,
321) -> Option<IntegerRange> {
322 if is_value_preserving_widening(source_type, target_type) {
323 Some(source_range)
324 } else {
325 integer_bounds(target_type)
326 }
327}
328
329const fn is_value_preserving_widening(
330 source_type: ElementaryType,
331 target_type: ElementaryType,
332) -> bool {
333 match (source_type, target_type) {
334 (ElementaryType::UInt(source), ElementaryType::UInt(target))
335 | (ElementaryType::Int(source), ElementaryType::Int(target)) => {
336 source.bits() <= target.bits()
337 }
338 _ => false,
339 }
340}
341
342fn expression_type<'hir>(
343 hir: &'hir hir::Hir<'hir>,
344 expr: &'hir hir::Expr<'hir>,
345) -> Option<ElementaryType> {
346 match &expr.peel_parens().kind {
347 ExprKind::Ident(resolutions) => {
348 if let Some(Res::Item(ItemId::Variable(variable))) = resolutions.first()
349 && let TypeKind::Elementary(ty) = hir.variable(*variable).ty.kind
350 {
351 return Some(ty);
352 }
353 }
354 ExprKind::Call(call_expr, _, _) => {
355 if let ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ty), .. }) =
356 &call_expr.kind
357 {
358 return Some(*ty);
359 }
360 }
361 _ => {}
362 }
363 None
364}
365
366fn lit_value_of(expr: &hir::Expr<'_>) -> Option<(bool, U256)> {
369 match &expr.peel_parens().kind {
370 ExprKind::Lit(lit) => {
371 if let LitKind::Number(n) = lit.kind {
372 return Some(normalize_zero(false, n));
373 }
374 None
375 }
376 ExprKind::Unary(op, inner) if op.kind == UnOpKind::Neg => {
377 if let ExprKind::Lit(lit) = &inner.peel_parens().kind
378 && let LitKind::Number(n) = lit.kind
379 {
380 return Some(normalize_zero(true, n));
381 }
382 None
383 }
384 _ => None,
385 }
386}
387
388fn normalize_zero(is_negative: bool, magnitude: U256) -> (bool, U256) {
389 if magnitude.is_zero() { (false, U256::ZERO) } else { (is_negative, magnitude) }
390}