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::{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 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 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 let mut intended: Vec<(ContractId, &BTreeSet<[u8; 4]>)> = Vec::new();
103 for (iid, isel) in &candidates {
104 if *iid == tid {
105 continue;
106 }
107 if target.linearized_bases.contains(iid) {
109 continue;
110 }
111 if !isel.is_subset(target_selectors) {
113 continue;
114 }
115 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 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 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
169fn 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
206fn 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}