Skip to main content

forge_lint/sol/info/
missing_inheritance.rs

1use crate::{
2    linter::{Lint, ProjectLintEmitter, ProjectLintPass, ProjectSource},
3    sol::{Severity, SolLint, info::MissingInheritance},
4};
5use solar::{
6    interface::source_map::FileName,
7    sema::hir::{ContractId, ContractKind, FunctionKind, Hir, ItemId},
8};
9use std::collections::{BTreeSet, HashMap};
10
11declare_forge_lint!(
12    MISSING_INHERITANCE,
13    Severity::Info,
14    "missing-inheritance",
15    "contract has all selectors of an interface it does not explicitly inherit"
16);
17
18impl<'ast> ProjectLintPass<'ast> for MissingInheritance {
19    fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]) {
20        if !ctx.is_lint_enabled(MISSING_INHERITANCE.id()) {
21            return;
22        }
23        let gcx = ctx.gcx();
24
25        // Only user-provided files are analyzed (and emitted against).
26        let input_source_idx: HashMap<_, _> = gcx
27            .hir
28            .sources_enumerated()
29            .filter_map(|(sid, src)| {
30                let FileName::Real(path) = &src.file.name else { return None };
31                Some((sid, sources.iter().position(|s| &s.path == path)?))
32            })
33            .collect();
34        if input_source_idx.is_empty() {
35            return;
36        }
37
38        // Targets are restricted to user input; candidates span the whole HIR so dependency
39        // interfaces (e.g. OpenZeppelin's `IERC20`) are still matched.
40        let mut selectors = HashMap::<ContractId, BTreeSet<[u8; 4]>>::new();
41        let mut candidates = Vec::new();
42        let mut targets = Vec::new();
43        for cid in gcx.hir.contract_ids() {
44            let contract = gcx.hir.contract(cid);
45            if contract.linearization_failed() {
46                continue;
47            }
48            let sels: BTreeSet<_> =
49                gcx.interface_functions(cid).all().iter().map(|f| f.selector.0).collect();
50            let interface_like = match contract.kind {
51                ContractKind::Interface => true,
52                ContractKind::AbstractContract => is_signature_only(&gcx.hir, cid),
53                ContractKind::Contract | ContractKind::Library => false,
54            };
55            if interface_like {
56                if !sels.is_empty() {
57                    candidates.push(cid);
58                }
59            } else if !contract.kind.is_library() && input_source_idx.contains_key(&contract.source)
60            {
61                targets.push(cid);
62            }
63            selectors.insert(cid, sels);
64        }
65
66        // Stable sort key for deterministic dedupe ordering across runs.
67        let sort_key = |cid| {
68            let name = &gcx.hir.contract(cid).name;
69            (name.span, name.as_str())
70        };
71        for tid in targets {
72            let target = gcx.hir.contract(tid);
73            let target_sels = &selectors[&tid];
74            if target_sels.is_empty() {
75                continue;
76            }
77            // The target must implement every selector of the candidate, without already
78            // inheriting it (transitively) or an inherited base covering the candidate.
79            let mut intended: Vec<ContractId> = candidates
80                .iter()
81                .copied()
82                .filter(|&iid| {
83                    let isel = &selectors[&iid];
84                    iid != tid
85                        && !target.linearized_bases.contains(&iid)
86                        && isel.is_subset(target_sels)
87                        && !target.linearized_bases.iter().any(|b| {
88                            *b != tid && selectors.get(b).is_some_and(|bsel| isel.is_subset(bsel))
89                        })
90                })
91                .collect();
92            // Deterministic dedupe by maximal selector set: sort by descending selector count,
93            // tie-break by (span, name), then drop any candidate whose selector set is a
94            // subset/superset of a kept one.
95            intended.sort_by(|&a, &b| {
96                selectors[&b]
97                    .len()
98                    .cmp(&selectors[&a].len())
99                    .then_with(|| sort_key(a).cmp(&sort_key(b)))
100            });
101            let mut kept: Vec<ContractId> = Vec::new();
102            for iid in intended {
103                let isel = &selectors[&iid];
104                if !kept.iter().any(|kid| {
105                    let ksel = &selectors[kid];
106                    isel.is_subset(ksel) || ksel.is_subset(isel)
107                }) {
108                    kept.push(iid);
109                }
110            }
111
112            let Some(&src_idx) = input_source_idx.get(&target.source) else { continue };
113            for iid in kept {
114                let msg = format!(
115                    "contract `{}` has all selectors of interface `{}` but does not explicitly inherit from it",
116                    target.name.as_str(),
117                    gcx.hir.contract(iid).name.as_str(),
118                );
119                ctx.emit_with_msg(&sources[src_idx], &MISSING_INHERITANCE, target.name.span, msg);
120            }
121        }
122    }
123}
124
125/// True if `cid` is an "interface-like" abstract contract: signature-only and free of state,
126/// constructors, and modifier bodies. Such contracts mirror the role of `interface` and are
127/// candidate interfaces for the missing-inheritance check.
128fn is_signature_only(hir: &Hir<'_>, cid: ContractId) -> bool {
129    let mut has_function = false;
130    for &item_id in hir.contract(cid).items {
131        match item_id {
132            ItemId::Variable(_) => return false,
133            ItemId::Function(fid) => {
134                let func = hir.function(fid);
135                match func.kind {
136                    FunctionKind::Function if func.body.is_none() => has_function = true,
137                    FunctionKind::Modifier if func.body.is_none() => {}
138                    _ => return false,
139                }
140            }
141            _ => {}
142        }
143    }
144    has_function
145}