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 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
54const fn is_filterable_field(param: &VariableDefinition<'_>) -> bool {
56 matches!(¶m.ty.kind, TypeKind::Elementary(ElementaryType::Address(_)))
57}
58
59fn describe_param(index: usize, param: &VariableDefinition<'_>) -> String {
61 let name = match ¶m.name {
62 Some(ident) => ident.as_str().to_string(),
63 None => format!("parameter #{}", index + 1),
64 };
65 let ty = type_str(¶m.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}