Skip to main content

forge_lint/sol/info/
modifier_used_only_once.rs

1use super::ModifierUsedOnlyOnce;
2use crate::{
3    linter::{Lint, ProjectLintEmitter, ProjectLintPass, ProjectSource},
4    sol::{Severity, SolLint},
5};
6use solar::{ast::FunctionKind, interface::source_map::FileName, sema::hir};
7use std::collections::HashMap;
8
9declare_forge_lint!(
10    MODIFIER_USED_ONLY_ONCE,
11    Severity::Info,
12    "modifier-used-only-once",
13    "this modifier is used only once; consider inlining its checks into the function"
14);
15
16impl<'ast> ProjectLintPass<'ast> for ModifierUsedOnlyOnce {
17    fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]) {
18        if !ctx.is_lint_enabled(MODIFIER_USED_ONLY_ONCE.id()) {
19            return;
20        }
21        let gcx = ctx.gcx();
22        let hir = &gcx.hir;
23
24        // Map every input source's HIR `SourceId` to the corresponding `ProjectSource` index:
25        // only modifiers declared in user-provided files are reported, while invocations are
26        // counted across the whole unit, dependencies included.
27        let input_source_idx: HashMap<hir::SourceId, usize> = hir
28            .sources_enumerated()
29            .filter_map(|(sid, src)| {
30                let path = match &src.file.name {
31                    FileName::Real(p) => p,
32                    _ => return None,
33                };
34                let idx = sources.iter().position(|s| &s.path == path)?;
35                Some((sid, idx))
36            })
37            .collect();
38
39        if input_source_idx.is_empty() {
40            return;
41        }
42
43        let counts = count_modifier_invocations(hir);
44
45        for function_id in hir.function_ids() {
46            let function = hir.function(function_id);
47            let Some(&src_idx) = input_source_idx.get(&function.source) else { continue };
48            // Only modifier declarations with a body qualify. `virtual` modifiers and
49            // overrides exist for dynamic dispatch, so inlining them is not an option and
50            // their invocation count does not tell the story.
51            if function.kind != FunctionKind::Modifier
52                || function.body.is_none()
53                || function.virtual_
54                || function.override_
55            {
56                continue;
57            }
58            // Exactly one invocation: zero invocations is dead code, a different concern.
59            if counts.get(&function_id).copied().unwrap_or(0) == 1 {
60                ctx.emit(&sources[src_idx], &MODIFIER_USED_ONLY_ONCE, function.keyword_span());
61            }
62        }
63    }
64}
65
66/// Counts, for every modifier of the unit, the functions that invoke it. Invocations live in
67/// each function's resolved modifier list, where base-constructor calls carry a contract id
68/// and stay out of the count.
69fn count_modifier_invocations(hir: &hir::Hir<'_>) -> HashMap<hir::FunctionId, usize> {
70    let mut counts = HashMap::new();
71    // Every function of the unit, constructors included, can invoke modifiers.
72    for function_id in hir.function_ids() {
73        for invocation in hir.function(function_id).modifiers {
74            if let hir::ItemId::Function(modifier_id) = invocation.id {
75                *counts.entry(modifier_id).or_insert(0) += 1;
76            }
77        }
78    }
79    counts
80}