Skip to main content

forge_lint/sol/info/
event_fields.rs

1use super::EventFields;
2use crate::{
3    linter::{EarlyLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::ast::{ElementaryType, Item, ItemEvent, ItemKind, Type, TypeKind, VariableDefinition};
7
8declare_forge_lint!(
9    EVENT_FIELDS,
10    Severity::Info,
11    "event-fields",
12    "address and id event parameters should be indexed for efficient log filtering"
13);
14
15/// Maximum number of indexed parameters allowed by the EVM in a non-anonymous event.
16const MAX_INDEXED_NON_ANON: usize = 3;
17/// Maximum number of indexed parameters allowed by the EVM in an anonymous event.
18const MAX_INDEXED_ANON: usize = 4;
19
20impl<'ast> EarlyLintPass<'ast> for EventFields {
21    fn check_item(&mut self, ctx: &LintContext, item: &'ast Item<'ast>) {
22        let ItemKind::Event(event) = &item.kind else { return };
23        check_event(ctx, event);
24    }
25}
26
27fn check_event<'ast>(ctx: &LintContext, event: &'ast ItemEvent<'ast>) {
28    if event.parameters.iter().any(|p| p.indexed) {
29        return;
30    }
31    let slots_available = if event.anonymous { MAX_INDEXED_ANON } else { MAX_INDEXED_NON_ANON };
32
33    // Collect offending unindexed params (with their positional index) in declaration order.
34    let mut offenders: Vec<(usize, &VariableDefinition<'ast>)> = Vec::new();
35    for (idx, param) in event.parameters.iter().enumerate() {
36        if is_filterable_field(param) {
37            offenders.push((idx, param));
38            if offenders.len() == slots_available {
39                break;
40            }
41        }
42    }
43
44    if offenders.is_empty() {
45        return;
46    }
47
48    // Build a single message naming the offending fields.
49    let names = offenders.iter().map(|(i, p)| describe_param(*i, p)).collect::<Vec<_>>().join(", ");
50    let msg = format!("event has unindexed fields that may benefit from being indexed: {names}");
51    ctx.emit_with_msg(&EVENT_FIELDS, event.name.span, msg);
52}
53
54/// Returns true when the parameter is an `address`, an `address payable`, or a uint256/bytes32
55/// whose name looks like an identifier (id-like).
56fn is_filterable_field(param: &VariableDefinition<'_>) -> bool {
57    let TypeKind::Elementary(elem) = &param.ty.kind else { return false };
58    match elem {
59        ElementaryType::Address(_) => true,
60        ElementaryType::UInt(size) if size.bits() == 256 => has_id_like_name(param),
61        ElementaryType::FixedBytes(size) if size.bytes() == 32 => has_id_like_name(param),
62        _ => false,
63    }
64}
65
66/// Returns true when the parameter name matches `id`/`ID`, ends with `Id`, `_id`, `_ID`, or ends
67/// with `ID` preceded by a lowercase ASCII letter.
68fn has_id_like_name(param: &VariableDefinition<'_>) -> bool {
69    let Some(ident) = &param.name else { return false };
70    let name = ident.as_str();
71
72    if name == "id" || name == "ID" {
73        return true;
74    }
75    if name.ends_with("_id") || name.ends_with("_ID") || name.ends_with("Id") {
76        return true;
77    }
78    if let Some(prefix) = name.strip_suffix("ID")
79        && let Some(last) = prefix.chars().last()
80    {
81        return last.is_ascii_lowercase();
82    }
83    false
84}
85
86/// Render a parameter as `name (type)` (or `parameter #N (type)` if unnamed) for the diagnostic.
87fn describe_param(index: usize, param: &VariableDefinition<'_>) -> String {
88    let name = match &param.name {
89        Some(ident) => ident.as_str().to_string(),
90        None => format!("parameter #{}", index + 1),
91    };
92    let ty = type_str(&param.ty);
93    format!("{name} ({ty})")
94}
95
96const fn type_str(ty: &Type<'_>) -> &'static str {
97    match &ty.kind {
98        TypeKind::Elementary(ElementaryType::Address(true)) => "address payable",
99        TypeKind::Elementary(ElementaryType::Address(false)) => "address",
100        TypeKind::Elementary(ElementaryType::UInt(_)) => "uint256",
101        TypeKind::Elementary(ElementaryType::FixedBytes(_)) => "bytes32",
102        _ => "?",
103    }
104}