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 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`.
55const fn is_filterable_field(param: &VariableDefinition<'_>) -> bool {
56    matches!(&param.ty.kind, TypeKind::Elementary(ElementaryType::Address(_)))
57}
58
59/// Render a parameter as `name (type)` (or `parameter #N (type)` if unnamed) for the diagnostic.
60fn describe_param(index: usize, param: &VariableDefinition<'_>) -> String {
61    let name = match &param.name {
62        Some(ident) => ident.as_str().to_string(),
63        None => format!("parameter #{}", index + 1),
64    };
65    let ty = type_str(&param.ty);
66    format!("{name} ({ty})")
67}
68
69const fn type_str(ty: &Type<'_>) -> &'static str {
70    match &ty.kind {
71        TypeKind::Elementary(ElementaryType::Address(true)) => "address payable",
72        TypeKind::Elementary(ElementaryType::Address(false)) => "address",
73        TypeKind::Elementary(ElementaryType::UInt(_)) => "uint256",
74        TypeKind::Elementary(ElementaryType::FixedBytes(_)) => "bytes32",
75        _ => "?",
76    }
77}