Skip to main content

forge_lint/sol/low/
missing_events_access_control.rs

1use super::MissingEventsAccessControl;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{
5        Severity, SolLint,
6        analysis::{
7            branch_always_exits, for_each_lhs_var, guard_vars, is_protected, is_sender_member,
8            is_zero_value, lhs_local_var, loop_stmts, referenced_item, underlying_var,
9        },
10    },
11};
12use solar::{
13    ast::{BinOpKind, ContractKind, DataLocation, StateMutability, Visibility},
14    interface::{Span, data_structures::Never},
15    sema::{
16        Gcx,
17        builtins::Builtin,
18        hir::{
19            self, EventId, Expr, ExprKind, FunctionId, ItemId, LoopSource, Stmt, StmtKind,
20            VariableId, Visit,
21        },
22    },
23};
24use std::{
25    collections::{HashMap, HashSet},
26    iter,
27    ops::ControlFlow,
28};
29
30declare_forge_lint!(
31    MISSING_EVENTS_ACCESS_CONTROL,
32    Severity::Low,
33    "missing-events-access-control",
34    "access control changes without an event"
35);
36
37impl<'gcx> LateLintPass<'gcx> for MissingEventsAccessControl {
38    fn check_contract(
39        &mut self,
40        ctx: &LintContext,
41        gcx: Gcx<'gcx>,
42        contract: &'gcx hir::Contract<'gcx>,
43    ) {
44        if !matches!(contract.kind, ContractKind::Contract | ContractKind::AbstractContract) {
45            return;
46        }
47
48        // Every state variable some access check in the contract depends on.
49        let functions: Vec<_> = contract.all_functions().collect();
50        let targets: HashSet<_> = functions.iter().flat_map(|&id| guard_vars(gcx, id)).collect();
51        if targets.is_empty() {
52            return;
53        }
54
55        for func_id in functions {
56            let func = gcx.hir.function(func_id);
57            let is_entry_point = func.kind.is_function()
58                && matches!(func.visibility, Visibility::Public | Visibility::External)
59                && !func.is_constructor()
60                && !func.is_special()
61                && !matches!(func.state_mutability, StateMutability::Pure | StateMutability::View);
62            if !is_entry_point || !is_protected(gcx, func_id) {
63                continue;
64            }
65
66            let guard_targets = guard_vars(gcx, func_id);
67            let mut analyzer = WriteAnalyzer {
68                gcx,
69                targets: &targets,
70                guard_targets: &guard_targets,
71                state: State {
72                    taint: func
73                        .parameters
74                        .iter()
75                        .map(|&p| (p, Sources::from([Source::Var(p)])))
76                        .collect(),
77                    ..Default::default()
78                },
79                call_stack: Vec::new(),
80                return_states: Vec::new(),
81                terminal_states: Vec::new(),
82                next_write_id: 0,
83            };
84            analyzer.analyze_function(func_id);
85
86            let mut emitted = HashSet::new();
87            for write in analyzer.state.writes {
88                if write.evented || !emitted.insert(write.var_id) {
89                    continue;
90                }
91                let name = gcx
92                    .hir
93                    .variable(write.var_id)
94                    .name
95                    .map_or_else(|| "state variable".to_string(), |name| name.to_string());
96                ctx.emit_with_msg(
97                    &MISSING_EVENTS_ACCESS_CONTROL,
98                    write.span,
99                    format!("`{name}` is changed without an event but is used for access control"),
100                );
101            }
102        }
103    }
104}
105
106/// Calls `f` on every index and slice bound along the spine of an lvalue.
107fn for_each_lhs_index<'gcx>(expr: &'gcx Expr<'gcx>, f: &mut impl FnMut(&'gcx Expr<'gcx>)) {
108    match &expr.peel_parens().kind {
109        ExprKind::Index(base, index) => {
110            for_each_lhs_index(base, f);
111            if let Some(index) = index {
112                f(index);
113            }
114        }
115        ExprKind::Slice(base, start, end) => {
116            for_each_lhs_index(base, f);
117            for bound in start.iter().chain(end) {
118                f(bound);
119            }
120        }
121        ExprKind::Member(base, _) | ExprKind::Payable(base) | ExprKind::Unary(_, base) => {
122            for_each_lhs_index(base, f)
123        }
124        ExprKind::Tuple(exprs) => exprs.iter().flatten().for_each(|e| for_each_lhs_index(e, f)),
125        _ => {}
126    }
127}
128
129// --- Writes without events --------------------------------------------------------------------
130
131/// Where a written value may come from: an entry-point parameter or state variable, or the caller.
132#[derive(Clone, Copy, PartialEq, Eq, Hash)]
133enum Source {
134    Var(VariableId),
135    Sender,
136}
137
138type Sources = HashSet<Source>;
139
140#[derive(Clone)]
141struct StateWrite {
142    id: usize,
143    var_id: VariableId,
144    span: Span,
145    sources: Sources,
146    /// The written value is a literal zero/false, so no source is needed for an event to match.
147    fixed_clear: bool,
148    evented: bool,
149}
150
151#[derive(Clone, Default)]
152struct State {
153    /// Sources each local may currently hold.
154    taint: HashMap<VariableId, Sources>,
155    /// Storage-pointer locals and the state variable they alias.
156    storage_aliases: HashMap<VariableId, VariableId>,
157    writes: Vec<StateWrite>,
158    /// Events emitted so far, not yet matched against every write reachable at this point.
159    emits: Vec<(EventId, Sources)>,
160}
161
162/// Collects writes to `targets` reachable from an entry point and marks those an `emit` covers.
163struct WriteAnalyzer<'a, 'gcx> {
164    gcx: Gcx<'gcx>,
165    targets: &'a HashSet<VariableId>,
166    /// Targets checked by this entry point's own guards; clearing one of them is reportable even
167    /// when the written value carries no source.
168    guard_targets: &'a HashSet<VariableId>,
169    state: State,
170    call_stack: Vec<FunctionId>,
171    /// State at each normal `return`, grouped by active inlined function call.
172    return_states: Vec<Vec<State>>,
173    /// State at each successful terminal exit, grouped by active inlined function call.
174    terminal_states: Vec<Vec<State>>,
175    next_write_id: usize,
176}
177
178impl<'gcx> WriteAnalyzer<'_, 'gcx> {
179    fn analyze_function(&mut self, func_id: FunctionId) {
180        if self.call_stack.contains(&func_id) {
181            return;
182        }
183        let func = self.gcx.hir.function(func_id);
184        let Some(body) = func.body else { return };
185        self.call_stack.push(func_id);
186        self.return_states.push(Vec::new());
187        self.terminal_states.push(Vec::new());
188        for modifier in func.modifiers {
189            if let Some(modifier_id) = modifier.id.as_function() {
190                let _ = self.visit_call_args(&modifier.args);
191                self.analyze_call(modifier_id, |index| modifier.args.exprs().nth(index));
192            }
193        }
194        for stmt in body.stmts {
195            let _ = self.visit_stmt(stmt);
196        }
197        self.correlate_pending();
198        let mut exits = self.return_states.pop().unwrap();
199        let terminal_exits = self.terminal_states.pop().unwrap();
200        if !exits.is_empty() || !terminal_exits.is_empty() {
201            if !body.stmts.iter().any(|stmt| branch_always_exits(self.gcx, stmt)) {
202                exits.push(self.state.clone());
203            }
204            for write in &mut self.state.writes {
205                // An exit before this write does not require an event for it.
206                let mut matching = exits
207                    .iter()
208                    .chain(&terminal_exits)
209                    .filter_map(|state| state.writes.iter().find(|other| other.id == write.id));
210                if let Some(first) = matching.next() {
211                    write.evented = first.evented && matching.all(|other| other.evented);
212                }
213            }
214            self.state.emits.retain(|event| exits.iter().all(|state| state.emits.contains(event)));
215        }
216        if let Some(parent) = self.terminal_states.last_mut() {
217            parent.extend(terminal_exits);
218        }
219        self.call_stack.pop();
220    }
221
222    /// Inlines `callee_id` with its parameters bound to argument sources; locals and storage
223    /// aliases are callee-private, pending writes flow back to the caller.
224    fn analyze_call(
225        &mut self,
226        callee_id: FunctionId,
227        mut argument: impl FnMut(usize) -> Option<&'gcx Expr<'gcx>>,
228    ) {
229        let params = self
230            .gcx
231            .hir
232            .function(callee_id)
233            .parameters
234            .iter()
235            .enumerate()
236            .filter_map(|(index, &param)| {
237                let sources = self.value_sources(argument(index)?);
238                (!sources.is_empty()).then_some((param, sources))
239            })
240            .collect();
241        let saved_taint = std::mem::replace(&mut self.state.taint, params);
242        let saved_aliases = std::mem::take(&mut self.state.storage_aliases);
243        self.analyze_function(callee_id);
244        self.state.taint = saved_taint;
245        self.state.storage_aliases = saved_aliases;
246    }
247
248    /// Sources flowing into `expr`: `msg.sender`, state variables and tainted locals.
249    fn value_sources(&self, expr: &Expr<'_>) -> Sources {
250        let mut out = Sources::new();
251        let _ = expr.visit(&mut |e| {
252            if is_sender_member(self.gcx, e) {
253                out.insert(Source::Sender);
254            }
255            if let Some(var_id) = underlying_var(self.gcx, e) {
256                if self.gcx.hir.variable(var_id).kind.is_state() {
257                    out.insert(Source::Var(var_id));
258                }
259                if let Some(sources) = self.state.taint.get(&var_id) {
260                    out.extend(sources);
261                }
262            }
263            ControlFlow::<()>::Continue(())
264        });
265        out
266    }
267
268    /// State variables written through `lhs`, resolving storage pointers to their roots.
269    fn lhs_state_vars(&self, lhs: &Expr<'_>) -> Vec<VariableId> {
270        let mut vars = Vec::new();
271        for_each_lhs_var(self.gcx, lhs, &mut |var_id| {
272            let root = if self.gcx.hir.variable(var_id).kind.is_state() {
273                Some(var_id)
274            } else {
275                self.state.storage_aliases.get(&var_id).copied()
276            };
277            if let Some(root) = root
278                && !vars.contains(&root)
279            {
280                vars.push(root);
281            }
282        });
283        vars
284    }
285
286    fn record_writes(&mut self, lhs: &Expr<'_>, sources: &Sources, fixed_clear: bool) {
287        for var_id in self.lhs_state_vars(lhs) {
288            if self.targets.contains(&var_id)
289                && (!sources.is_empty() || (fixed_clear && self.guard_targets.contains(&var_id)))
290            {
291                self.state.writes.push(StateWrite {
292                    id: self.next_write_id,
293                    var_id,
294                    span: lhs.span,
295                    sources: sources.clone(),
296                    fixed_clear,
297                    evented: false,
298                });
299                self.next_write_id += 1;
300            }
301        }
302    }
303
304    fn set_taint(&mut self, var_id: VariableId, sources: Sources) {
305        if sources.is_empty() {
306            self.state.taint.remove(&var_id);
307        } else {
308            self.state.taint.insert(var_id, sources);
309        }
310    }
311
312    /// Records `var_id = value`, tracking which state variable a storage pointer aliases.
313    fn set_local(&mut self, var_id: VariableId, sources: Sources, value: &Expr<'_>) {
314        self.set_taint(var_id, sources);
315        let root = (self.gcx.hir.variable(var_id).data_location == Some(DataLocation::Storage))
316            .then(|| self.lhs_state_vars(value).into_iter().next())
317            .flatten();
318        match root {
319            Some(root) => self.state.storage_aliases.insert(var_id, root),
320            None => self.state.storage_aliases.remove(&var_id),
321        };
322    }
323
324    /// Records an `emit` as pending, to be matched against writes by `correlate_pending`. Emits
325    /// are matched independent of statement order within the same straight-line scope, so an
326    /// `emit` written before the state change it documents is still recognized.
327    fn record_emit(&mut self, expr: &Expr<'_>) {
328        let Some(event_id) = emitted_event_id(self.gcx, expr) else { return };
329        let event_sources = self.value_sources(expr);
330        self.state.emits.push((event_id, event_sources));
331    }
332
333    /// Matches every pending emit against every not-yet-evented write reachable so far: the
334    /// event must mention the variable and share a source with the write (or the write must be
335    /// a fixed clear).
336    fn correlate_pending(&mut self) {
337        correlate_pending(self.gcx, &mut self.state);
338    }
339}
340
341impl<'gcx> Visit<'gcx> for WriteAnalyzer<'_, 'gcx> {
342    type BreakValue = Never;
343
344    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
345        &self.gcx.hir
346    }
347
348    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
349        match stmt.kind {
350            StmtKind::DeclSingle(var_id) => {
351                if let Some(init) = self.gcx.hir.variable(var_id).initializer {
352                    self.visit_expr(init)?;
353                    let sources = self.value_sources(init);
354                    self.set_local(var_id, sources, init);
355                }
356            }
357            StmtKind::DeclMulti(vars, expr) => {
358                self.visit_expr(expr)?;
359                let sources = self.value_sources(expr);
360                for var_id in vars.iter().flatten() {
361                    self.set_taint(*var_id, sources.clone());
362                }
363            }
364            StmtKind::If(cond, then_stmt, else_stmt) => {
365                self.visit_expr(cond)?;
366                let base = self.state.clone();
367                self.visit_stmt(then_stmt)?;
368                self.correlate_pending();
369                let then_state = std::mem::replace(&mut self.state, base.clone());
370                if let Some(else_stmt) = else_stmt {
371                    self.visit_stmt(else_stmt)?;
372                    self.correlate_pending();
373                }
374                let else_state = std::mem::take(&mut self.state);
375                self.state = merge_branches(
376                    base,
377                    then_state,
378                    else_state,
379                    branch_always_exits(self.gcx, then_stmt),
380                    else_stmt.is_some_and(|expr| branch_always_exits(self.gcx, expr)),
381                );
382            }
383            StmtKind::Emit(expr) => {
384                self.visit_expr(expr)?;
385                self.record_emit(expr);
386            }
387            StmtKind::Return(expr) => {
388                if let Some(expr) = expr {
389                    self.visit_expr(expr)?;
390                }
391                let mut state = self.state.clone();
392                correlate_pending(self.gcx, &mut state);
393                self.return_states.last_mut().unwrap().push(state);
394            }
395            StmtKind::Loop(block, source) => {
396                // A loop body may not run to completion on the iteration containing an emit — a
397                // `for`/`while` may run zero times, and even a `do-while` (which always starts
398                // its body) can `break`/`continue`/`revert` past the emit on that first pass — so
399                // an emit inside any loop must not survive to satisfy a write after the loop, and
400                // must not retroactively mark a write from BEFORE the loop as evented either.
401                let n = self.state.writes.len();
402                let evented_before: Vec<bool> =
403                    self.state.writes.iter().map(|w| w.evented).collect();
404                let emits_before = self.state.emits.clone();
405                if let LoopSource::For { update: Some(update) } = source
406                    && let [stmt] = block.stmts
407                    && let StmtKind::If(cond, body, Some(exit)) = stmt.kind
408                    && matches!(exit.kind, StmtKind::Break)
409                {
410                    // Solar lowers the loop condition to `if (cond) body else break`.
411                    // The update belongs to the continuing arm, so correlate its writes
412                    // with body events before discarding that arm's pending emits.
413                    self.visit_expr(cond)?;
414                    let base = self.state.clone();
415                    self.visit_stmt(body)?;
416                    self.visit_stmt(update)?;
417                    self.correlate_pending();
418                    let body_state = std::mem::take(&mut self.state);
419                    self.state = merge_branches(base.clone(), body_state, base, false, false);
420                } else {
421                    for stmt in loop_stmts(block, source) {
422                        self.visit_stmt(stmt)?;
423                    }
424                }
425                self.correlate_pending();
426                for (write, was_evented) in self.state.writes[..n].iter_mut().zip(evented_before) {
427                    write.evented = was_evented;
428                }
429                self.state.emits = emits_before;
430            }
431            StmtKind::Try(try_stmt) => {
432                self.visit_expr(&try_stmt.expr)?;
433                // Clauses are mutually exclusive: at most one runs. Each gets its own isolated
434                // copy of the pre-try state so one clause's emit can neither satisfy another
435                // clause's write nor a write from before the try; `merge_try_clauses` recombines
436                // them the same way `merge_branches` recombines an `if`'s two arms.
437                let base = self.state.clone();
438                let mut clause_states = Vec::with_capacity(try_stmt.clauses.len());
439                let mut continues = Vec::with_capacity(try_stmt.clauses.len());
440                for clause in try_stmt.clauses {
441                    self.state = base.clone();
442                    for stmt in clause.block.stmts {
443                        self.visit_stmt(stmt)?;
444                    }
445                    self.correlate_pending();
446                    clause_states.push(std::mem::take(&mut self.state));
447                    continues.push(
448                        !clause.block.stmts.iter().any(|stmt| branch_always_exits(self.gcx, stmt)),
449                    );
450                }
451                self.state = merge_try_clauses(base, clause_states, continues);
452            }
453            _ => return self.walk_stmt(stmt),
454        }
455        ControlFlow::Continue(())
456    }
457
458    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
459        match &expr.kind {
460            ExprKind::Assign(lhs, op, rhs) => {
461                self.visit_expr(rhs)?;
462                self.visit_expr(lhs)?;
463                let mut sources = self.value_sources(rhs);
464                for_each_lhs_index(lhs, &mut |index| sources.extend(self.value_sources(index)));
465                if op.is_some() {
466                    sources.extend(self.value_sources(lhs));
467                }
468                let local = lhs_local_var(self.gcx, lhs);
469                let rebinds_storage = op.is_none()
470                    && local.is_some_and(|var_id| {
471                        self.gcx.hir.variable(var_id).data_location == Some(DataLocation::Storage)
472                    })
473                    && self
474                        .gcx
475                        .type_of_expr(rhs.peel_parens().id)
476                        .is_some_and(|ty| ty.loc() == Some(DataLocation::Storage));
477                if !rebinds_storage {
478                    self.record_writes(lhs, &sources, is_zero_value(rhs));
479                }
480                if let Some(local) = local {
481                    self.set_local(local, sources, rhs);
482                }
483                ControlFlow::Continue(())
484            }
485            ExprKind::Delete(inner) => {
486                let mut sources = Sources::new();
487                for_each_lhs_index(inner, &mut |index| sources.extend(self.value_sources(index)));
488                self.record_writes(inner, &sources, true);
489                self.walk_expr(expr)
490            }
491            ExprKind::Call(callee, ..) => {
492                self.walk_expr(expr)?;
493                if matches!(callee.peel_parens().kind, ExprKind::Ident(_))
494                    && let Some(callee_id) = self.gcx.resolved_function(callee)
495                {
496                    let gcx = self.gcx;
497                    self.analyze_call(callee_id, |index| gcx.call_arg(expr, index));
498                }
499                if self.gcx.resolved_builtin(callee) == Some(Builtin::Selfdestruct) {
500                    let mut state = self.state.clone();
501                    correlate_pending(self.gcx, &mut state);
502                    self.terminal_states.last_mut().unwrap().push(state);
503                }
504                ControlFlow::Continue(())
505            }
506            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
507                self.visit_expr(lhs)?;
508                self.correlate_pending();
509                let base = self.state.clone();
510                self.visit_expr(rhs)?;
511                self.correlate_pending();
512                let rhs_state = std::mem::take(&mut self.state);
513                // The RHS may be skipped, so its events cannot cover writes on that path.
514                self.state = merge_branches(base.clone(), rhs_state, base, false, false);
515                ControlFlow::Continue(())
516            }
517            ExprKind::Ternary(cond, then_expr, else_expr) => {
518                self.visit_expr(cond)?;
519                self.correlate_pending();
520                let base = self.state.clone();
521                self.visit_expr(then_expr)?;
522                self.correlate_pending();
523                let then_state = std::mem::replace(&mut self.state, base.clone());
524                self.visit_expr(else_expr)?;
525                self.correlate_pending();
526                let else_state = std::mem::take(&mut self.state);
527                self.state = merge_branches(base, then_state, else_state, false, false);
528                ControlFlow::Continue(())
529            }
530            _ => self.walk_expr(expr),
531        }
532    }
533}
534
535/// Joins the two arms of an `if`: a pending write stays covered only if both arms emitted for it,
536/// while taint and aliases come from whichever arms can continue past the `if`. Emits recorded
537/// inside either arm are dropped rather than carried past the merge: `correlate_pending` already
538/// resolved them against that arm's own writes before this runs, and code after the `if` executes
539/// whichever branch ran (or neither), so an emit conditional on one arm must not retroactively
540/// satisfy a write reachable only outside it.
541fn merge_branches(
542    base: State,
543    then_state: State,
544    else_state: State,
545    then_exits: bool,
546    else_exits: bool,
547) -> State {
548    let emits = base.emits.clone();
549    let mut writes = base.writes;
550    for (i, write) in writes.iter_mut().enumerate() {
551        write.evented = then_state.writes[i].evented && else_state.writes[i].evented;
552    }
553    let n = writes.len();
554    writes.extend_from_slice(&then_state.writes[n..]);
555    writes.extend_from_slice(&else_state.writes[n..]);
556
557    let (taint, storage_aliases) = match (then_exits, else_exits) {
558        (true, true) => (base.taint, base.storage_aliases),
559        (true, false) => (else_state.taint, else_state.storage_aliases),
560        (false, true) => (then_state.taint, then_state.storage_aliases),
561        (false, false) => {
562            let mut taint = then_state.taint;
563            for (var_id, sources) in else_state.taint {
564                taint.entry(var_id).or_default().extend(sources);
565            }
566            let storage_aliases = then_state
567                .storage_aliases
568                .into_iter()
569                .filter(|(alias, root)| else_state.storage_aliases.get(alias) == Some(root))
570                .collect();
571            (taint, storage_aliases)
572        }
573    };
574    State { taint, storage_aliases, writes, emits }
575}
576
577/// Joins mutually exclusive try/catch clauses. Pre-existing writes need an event in every
578/// clause, including exiting clauses: a `return` preserves writes, unlike a `revert`.
579/// Taint and aliases only merge from continuing clauses; clause-local emits are discarded.
580fn merge_try_clauses(base: State, clause_states: Vec<State>, continues: Vec<bool>) -> State {
581    let n = base.writes.len();
582    let mut writes = base.writes;
583    for (i, write) in writes.iter_mut().enumerate() {
584        write.evented = clause_states.iter().all(|state| state.writes[i].evented);
585    }
586    for state in &clause_states {
587        writes.extend_from_slice(&state.writes[n..]);
588    }
589
590    let continuing = || clause_states.iter().zip(&continues).filter(|&(_, &c)| c).map(|(s, _)| s);
591
592    let (taint, storage_aliases) = match continuing().next() {
593        None => (base.taint, base.storage_aliases),
594        Some(first) => {
595            let mut taint = HashMap::new();
596            for state in continuing() {
597                for (&var_id, sources) in &state.taint {
598                    taint
599                        .entry(var_id)
600                        .or_insert_with(Sources::new)
601                        .extend(sources.iter().copied());
602                }
603            }
604            let mut storage_aliases = first.storage_aliases.clone();
605            for state in continuing().skip(1) {
606                storage_aliases
607                    .retain(|alias, root| state.storage_aliases.get(alias) == Some(root));
608            }
609            (taint, storage_aliases)
610        }
611    };
612
613    State { taint, storage_aliases, writes, emits: base.emits }
614}
615
616fn emitted_event_id(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<EventId> {
617    let ExprKind::Call(callee, ..) = &expr.peel_parens().kind else { return None };
618    match referenced_item(gcx, callee)? {
619        ItemId::Event(event_id) => Some(event_id),
620        _ => None,
621    }
622}
623
624/// Whether the event name or one of its parameter names mentions the state variable: its
625/// normalized name, its singular form, or a role keyword it contains.
626fn event_mentions_state_var(gcx: Gcx<'_>, event_id: EventId, var_id: VariableId) -> bool {
627    let Some(var_name) = gcx.hir.variable(var_id).name else { return false };
628    let var_name = normalize(var_name.as_str());
629    let mut keywords = vec![var_name.as_str()];
630    keywords.extend(var_name.strip_suffix('s').filter(|singular| !singular.is_empty()));
631    let roles = ["owner", "admin", "guardian", "manager", "role"];
632    keywords.extend(roles.into_iter().filter(|role| var_name.contains(role)));
633
634    let event = gcx.hir.event(event_id);
635    let param_names = event.parameters.iter().filter_map(|&p| gcx.hir.variable(p).name);
636    iter::once(event.name).chain(param_names).any(|name| {
637        let name = normalize(name.as_str());
638        keywords.iter().any(|keyword| !keyword.is_empty() && name.contains(keyword))
639    })
640}
641
642fn normalize(name: &str) -> String {
643    name.chars().filter(char::is_ascii_alphanumeric).map(|c| c.to_ascii_lowercase()).collect()
644}
645
646fn correlate_pending(gcx: Gcx<'_>, state: &mut State) {
647    for (event_id, event_sources) in &state.emits {
648        for write in &mut state.writes {
649            if !write.evented
650                && (write.fixed_clear || !write.sources.is_disjoint(event_sources))
651                && event_mentions_state_var(gcx, *event_id, write.var_id)
652            {
653                write.evented = true;
654            }
655        }
656    }
657}