Skip to main content

prebindgen_registry/
diagnostics.rs

1//! What a binding did **not** claim, reported to the build log.
2//!
3//! Which items a binding skipped says nothing about which conversions it needs,
4//! so none of this belongs to [`Registry`](crate::registry::Registry)
5//! — that derives a crossing set from what **is** declared, and an ignore has no
6//! effect on that set. What ignores are for is suppressing a report: telling
7//! "you meant to skip this" apart from "you forgot this".
8//!
9//! So the ignores live here, with the reporting, and a generator calls
10//! [`warn_unclaimed`] itself.
11
12use std::collections::HashSet;
13
14use prebindgen_flat::flat::Flat;
15
16use crate::{prebindgen::NamePredicate, registry::TypeKey};
17
18/// What a binding claimed, so everything else can be reported.
19///
20/// The two populations are separate on purpose. A **declared** item is claimed
21/// and emitted; an **ignored** one is claimed and deliberately dropped. Both
22/// silence the skip report, but only an ignore that matches nothing is itself
23/// worth a warning — a declaration that matches nothing is a hard error the
24/// registry raises long before this runs.
25#[derive(Default)]
26pub struct Claimed {
27    /// Functions the binding emits, plus the helpers it only references.
28    pub functions: HashSet<syn::Ident>,
29    /// Types the binding emits, plus the ones that cross only through a plan.
30    pub types: HashSet<TypeKey>,
31    /// Consts the binding emits, or `None` when it has no const mechanism at
32    /// all — then every const is re-emitted verbatim, so none is ever skipped
33    /// and reporting one would be a lie.
34    pub consts: Option<HashSet<syn::Ident>>,
35    pub ignored_functions: HashSet<syn::Ident>,
36    pub ignored_types: HashSet<TypeKey>,
37    pub ignored_consts: HashSet<syn::Ident>,
38    /// Bulk ignores keyed on a naming family rather than an exact ident.
39    /// Kind-agnostic: prebindgen names live in one flat namespace.
40    pub ignored_name_predicates: Vec<NamePredicate>,
41}
42
43impl Claimed {
44    /// Whether a bulk-ignore predicate covers this name.
45    ///
46    /// A predicate matching nothing is silent by design — it is a filter, not a
47    /// claim, and its match count varies across feature configurations.
48    fn predicate_ignored(&self, name: &str) -> bool {
49        !self.ignored_name_predicates.is_empty()
50            && self.ignored_name_predicates.iter().any(|p| p(name))
51    }
52}
53
54/// Print one `cargo:warning=` line per captured item this binding never
55/// claimed, and per ignore entry that matches nothing.
56pub fn warn_unclaimed(flat: &Flat, claimed: &Claimed) {
57    for line in unclaimed_report(flat, claimed) {
58        println!("cargo:warning={line}");
59    }
60}
61
62/// The report itself, as lines — so it can be asserted on rather than scraped
63/// off stdout. Sorted within each group, so a build says the same thing twice.
64pub(crate) fn unclaimed_report(flat: &Flat, claimed: &Claimed) -> Vec<String> {
65    let mut out = Vec::new();
66
67    // Stale ignores: an entry naming nothing is a build.rs that has drifted
68    // from its source crate.
69    for ident in sorted(claimed.ignored_functions.iter().map(|i| i.to_string())) {
70        if flat.function(&ident_of(&ident)).is_none() {
71            out.push(format!(
72                "prebindgen: ignored function `{ident}` not found among #[prebindgen] items"
73            ));
74        }
75    }
76    for key in sorted(claimed.ignored_types.iter().map(|k| k.as_str().to_owned())) {
77        let named = TypeKey::parse(&key)
78            .ok()
79            .and_then(|k| k.ident())
80            .is_some_and(|ident| flat.declared_type(&ident).is_some());
81        if !named {
82            out.push(format!(
83                "prebindgen: ignored type `{key}` not found among #[prebindgen] items"
84            ));
85        }
86    }
87    if claimed.consts.is_some() {
88        for ident in sorted(claimed.ignored_consts.iter().map(|i| i.to_string())) {
89            if flat.constant(&ident_of(&ident)).is_none() {
90                out.push(format!(
91                    "prebindgen: ignored const `{ident}` not found among #[prebindgen] items"
92                ));
93            }
94        }
95    }
96
97    for name in sorted(
98        flat.functions()
99            .map(|f| &f.name)
100            .filter(|k| !claimed.functions.contains(*k) && !claimed.ignored_functions.contains(*k))
101            .map(|k| k.to_string())
102            .filter(|n| !claimed.predicate_ignored(n)),
103    ) {
104        out.push(format!(
105            "prebindgen: skipping undeclared #[prebindgen] fn `{name}`"
106        ));
107    }
108
109    // Struct/enum only — an alias is deliberately absent, because the message
110    // names a kind an alias is not.
111    for name in sorted(
112        struct_enum_idents(flat)
113            .filter(|i| {
114                let key = TypeKey::from_ident(i);
115                !claimed.types.contains(&key) && !claimed.ignored_types.contains(&key)
116            })
117            .map(|i| i.to_string())
118            .filter(|n| !claimed.predicate_ignored(n)),
119    ) {
120        out.push(format!(
121            "prebindgen: skipping undeclared #[prebindgen] struct/enum `{name}`"
122        ));
123    }
124
125    if let Some(declared) = &claimed.consts {
126        for name in sorted(
127            flat.constants()
128                .map(|c| &c.name)
129                .filter(|k| !declared.contains(*k) && !claimed.ignored_consts.contains(*k))
130                .map(|k| k.to_string())
131                .filter(|n| !claimed.predicate_ignored(n)),
132        ) {
133            out.push(format!(
134                "prebindgen: skipping undeclared #[prebindgen] const `{name}`"
135            ));
136        }
137    }
138
139    out
140}
141
142/// Every **struct or enum** name — either enum shape, never an alias.
143fn struct_enum_idents(flat: &Flat) -> impl Iterator<Item = &syn::Ident> {
144    use prebindgen_flat::flat::Type;
145    flat.types().filter_map(|t| match t {
146        Type::Struct(_) | Type::Variant(_) | Type::Enum(_) => Some(t.name()),
147        Type::Extern(_) => None,
148    })
149}
150
151fn sorted(it: impl Iterator<Item = String>) -> Vec<String> {
152    let mut v: Vec<String> = it.collect();
153    v.sort();
154    v
155}
156
157fn ident_of(name: &str) -> syn::Ident {
158    syn::Ident::new(name, proc_macro2::Span::call_site())
159}
160
161#[cfg(test)]
162mod tests;