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"
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
22        // Only modifiers declared in user-provided files are reported, while invocations are
23        // counted across the whole unit, dependencies included.
24        let input_source_idx: HashMap<_, _> = ctx
25            .gcx()
26            .hir
27            .sources_enumerated()
28            .filter_map(|(sid, src)| {
29                let FileName::Real(path) = &src.file.name else { return None };
30                Some((sid, sources.iter().position(|s| &s.path == path)?))
31            })
32            .collect();
33        if input_source_idx.is_empty() {
34            return;
35        }
36
37        // Invocations live in each function's resolved modifier list, where base-constructor
38        // calls carry a contract id and stay out of the count.
39        let mut counts = HashMap::<hir::FunctionId, usize>::new();
40        for function_id in ctx.gcx().hir.function_ids() {
41            for invocation in ctx.gcx().hir.function(function_id).modifiers {
42                if let hir::ItemId::Function(modifier_id) = invocation.id {
43                    *counts.entry(modifier_id).or_default() += 1;
44                }
45            }
46        }
47
48        for function_id in ctx.gcx().hir.function_ids() {
49            let function = ctx.gcx().hir.function(function_id);
50            let Some(&src_idx) = input_source_idx.get(&function.source) else { continue };
51            // Only modifier declarations with a body qualify. `virtual` modifiers and overrides
52            // exist for dynamic dispatch, so inlining them is not an option. Exactly one
53            // invocation: zero invocations is dead code, a different concern.
54            if function.kind == FunctionKind::Modifier
55                && function.body.is_some()
56                && !function.virtual_
57                && !function.override_
58                && counts.get(&function_id) == Some(&1)
59            {
60                ctx.emit(&sources[src_idx], &MODIFIER_USED_ONLY_ONCE, function.keyword_span());
61            }
62        }
63    }
64}