forge_lint/sol/info/
missing_inheritance.rs1use 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 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 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 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 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 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
125fn 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}