forge_lint/sol/info/
event_fields.rs1use 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
15const MAX_INDEXED_NON_ANON: usize = 3;
17const 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 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 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
54fn is_filterable_field(param: &VariableDefinition<'_>) -> bool {
57 let TypeKind::Elementary(elem) = ¶m.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
66fn has_id_like_name(param: &VariableDefinition<'_>) -> bool {
69 let Some(ident) = ¶m.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
86fn describe_param(index: usize, param: &VariableDefinition<'_>) -> String {
88 let name = match ¶m.name {
89 Some(ident) => ident.as_str().to_string(),
90 None => format!("parameter #{}", index + 1),
91 };
92 let ty = type_str(¶m.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}