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::{Span, source_map::FileName},
7    sema::{
8        Gcx,
9        hir::{ContractId, ContractKind, FunctionKind, ItemId, SourceId},
10    },
11};
12use std::collections::{BTreeSet, HashMap};
13
14declare_forge_lint!(
15    MISSING_INHERITANCE,
16    Severity::Info,
17    "missing-inheritance",
18    "contract implements an interface's external API but does not explicitly inherit from it"
19);
20
21impl<'ast> ProjectLintPass<'ast> for MissingInheritance {
22    fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]) {
23        if !ctx.is_lint_enabled(MISSING_INHERITANCE.id()) {
24            return;
25        }
26
27        let gcx = ctx.gcx();
28        let hir = &gcx.hir;
29
30        // Map every input source's HIR `SourceId` to the corresponding `ProjectSource` index, so
31        // we only analyze (and emit against) user-provided files.
32        let input_source_idx: HashMap<SourceId, usize> = hir
33            .sources_enumerated()
34            .filter_map(|(sid, src)| {
35                let path = match &src.file.name {
36                    FileName::Real(p) => p,
37                    _ => return None,
38                };
39                let idx = sources.iter().position(|s| &s.path == path)?;
40                Some((sid, idx))
41            })
42            .collect();
43
44        if input_source_idx.is_empty() {
45            return;
46        }
47
48        // Targets are restricted to user input; candidates span the whole HIR so dependency
49        // interfaces (e.g. OpenZeppelin's `IERC20`) are still matched.
50        let mut candidates: Vec<(ContractId, BTreeSet<[u8; 4]>)> = Vec::new();
51        let mut targets: Vec<ContractId> = Vec::new();
52        let mut selectors_by_contract: HashMap<ContractId, BTreeSet<[u8; 4]>> = HashMap::new();
53
54        for cid in hir.contract_ids() {
55            let contract = hir.contract(cid);
56            if contract.linearization_failed() {
57                continue;
58            }
59
60            let selectors: BTreeSet<[u8; 4]> =
61                gcx.interface_functions(cid).all().iter().map(|f| f.selector.0).collect();
62            selectors_by_contract.insert(cid, selectors.clone());
63
64            let in_input = input_source_idx.contains_key(&contract.source);
65
66            match contract.kind {
67                ContractKind::Library => {}
68                ContractKind::Interface => {
69                    if !selectors.is_empty() {
70                        candidates.push((cid, selectors));
71                    }
72                }
73                ContractKind::AbstractContract => {
74                    if is_signature_only(gcx, cid) {
75                        if !selectors.is_empty() {
76                            candidates.push((cid, selectors));
77                        }
78                    } else if in_input {
79                        targets.push(cid);
80                    }
81                }
82                ContractKind::Contract => {
83                    if in_input {
84                        targets.push(cid);
85                    }
86                }
87            }
88        }
89
90        if candidates.is_empty() || targets.is_empty() {
91            return;
92        }
93
94        for tid in targets {
95            let target = hir.contract(tid);
96            let Some(target_selectors) = selectors_by_contract.get(&tid) else { continue };
97            if target_selectors.is_empty() {
98                continue;
99            }
100
101            // Collect intended interfaces for this target.
102            let mut intended: Vec<(ContractId, &BTreeSet<[u8; 4]>)> = Vec::new();
103            for (iid, isel) in &candidates {
104                if *iid == tid {
105                    continue;
106                }
107                // Skip if already inherited (transitively).
108                if target.linearized_bases.contains(iid) {
109                    continue;
110                }
111                // Target must implement every selector of the candidate.
112                if !isel.is_subset(target_selectors) {
113                    continue;
114                }
115                // Skip if some inherited base of the target already covers the candidate.
116                let subsumed_by_base =
117                    target.linearized_bases.iter().filter(|b| **b != tid).any(|b| {
118                        match selectors_by_contract.get(b) {
119                            Some(bsel) => isel.is_subset(bsel),
120                            None => false,
121                        }
122                    });
123                if subsumed_by_base {
124                    continue;
125                }
126                intended.push((*iid, isel));
127            }
128
129            if intended.is_empty() {
130                continue;
131            }
132
133            // Deterministic dedupe by maximal selector set:
134            // sort by descending selector count, tie-break by (path, contract name, id), then
135            // drop any candidate whose selector set is a subset/superset of a kept one.
136            intended.sort_by(|(a_id, a_sel), (b_id, b_sel)| {
137                b_sel
138                    .len()
139                    .cmp(&a_sel.len())
140                    .then_with(|| sort_key(hir, *a_id).cmp(&sort_key(hir, *b_id)))
141            });
142
143            let mut kept: Vec<(ContractId, &BTreeSet<[u8; 4]>)> = Vec::new();
144            'outer: for (iid, isel) in intended {
145                for (_, ksel) in &kept {
146                    if isel.is_subset(ksel) || ksel.is_subset(isel) {
147                        continue 'outer;
148                    }
149                }
150                kept.push((iid, isel));
151            }
152
153            // Emit one diagnostic per kept interface, against the source containing the target.
154            let Some(&src_idx) = input_source_idx.get(&target.source) else { continue };
155            let source = &sources[src_idx];
156            for (iid, _) in kept {
157                let interface = hir.contract(iid);
158                let msg = format!(
159                    "contract `{}` implements interface `{}`'s external API but does not explicitly inherit from it",
160                    target.name.as_str(),
161                    interface.name.as_str(),
162                );
163                ctx.emit_with_msg(source, &MISSING_INHERITANCE, target.name.span, msg);
164            }
165        }
166    }
167}
168
169/// Returns `true` if `cid` is an "interface-like" abstract contract: signature-only and free of
170/// state, constructors, and modifier bodies. Such contracts mirror the role of `interface` and
171/// should be considered as candidate interfaces for the missing-inheritance check.
172fn is_signature_only<'gcx>(gcx: Gcx<'gcx>, cid: ContractId) -> bool {
173    let hir = &gcx.hir;
174    let contract = hir.contract(cid);
175
176    let mut has_function = false;
177    for &item_id in contract.items {
178        match item_id {
179            ItemId::Variable(_) => return false,
180            ItemId::Function(fid) => {
181                let func = hir.function(fid);
182                match func.kind {
183                    FunctionKind::Constructor | FunctionKind::Receive | FunctionKind::Fallback => {
184                        return false;
185                    }
186                    FunctionKind::Modifier => {
187                        if func.body.is_some() {
188                            return false;
189                        }
190                    }
191                    FunctionKind::Function => {
192                        if func.body.is_some() {
193                            return false;
194                        }
195                        has_function = true;
196                    }
197                }
198            }
199            _ => {}
200        }
201    }
202
203    has_function
204}
205
206/// Stable sort key for deterministic dedupe ordering across runs.
207fn sort_key<'hir>(hir: &'hir solar::sema::hir::Hir<'hir>, cid: ContractId) -> (Span, &'hir str) {
208    let c = hir.contract(cid);
209    (c.name.span, c.name.as_str())
210}