Skip to main content

prebindgen_flat/flat/
key.rs

1//! The canonical identity of a type: its normalized token string.
2
3use std::fmt;
4
5use quote::ToTokens;
6
7/// Canonical type-shape key: identity is the token string of the
8/// **normalized** type. Normalization is a closed rule set — group/paren
9/// unwrap, a `crate::`/`self::`/source-module path reduced to its final
10/// segment, and a prelude path read as the bare name the language knows it
11/// by (`std::vec::Vec<Foo>` ≡ `Vec<Foo>`) — and any spelling it does not
12/// cover is kept verbatim.
13///
14/// # A key is an identity, and nothing else
15///
16/// It is what a table is indexed by. It is **not** a route to `syn::Type`: the
17/// only way to reach a type's syntax is
18/// `Conversions::reading` (in the registry layer above) followed by
19/// [`TypeRef`](super::TypeRef), because a
20/// reading is what pairs a spelling with the classification that vouches for
21/// it.
22///
23/// This used to keep the parsed form beside the string and hand it out through
24/// `to_type()`, which let any holder of a key produce tokens for a type the
25/// model never classified — the same capability #280 sealed `TypeRef` against,
26/// granted by the key itself. A caller that wants tokens now has to have gotten
27/// them from somewhere that knows what they mean: the registry's reading, or
28/// the declaration that wrote them (#291).
29///
30/// What a key can still answer about itself is what it is **called** —
31/// [`Self::as_str`], [`Self::ident`], [`Self::short_name`] — because a name is
32/// not syntax.
33#[derive(Clone)]
34pub struct TypeKey {
35    /// Canonical token string — the identity `Eq`/`Hash` compare, and the whole
36    /// of what a key is.
37    canon: std::rc::Rc<str>,
38}
39
40impl PartialEq for TypeKey {
41    fn eq(&self, other: &Self) -> bool {
42        self.canon == other.canon
43    }
44}
45impl Eq for TypeKey {}
46impl std::hash::Hash for TypeKey {
47    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
48        self.canon.hash(state)
49    }
50}
51impl PartialOrd for TypeKey {
52    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
53        Some(self.cmp(other))
54    }
55}
56impl Ord for TypeKey {
57    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
58        self.canon.cmp(&other.canon)
59    }
60}
61// Keep the historical single-field tuple rendering (`TypeKey("Vec < u8 >")`)
62// — error text and test expectations format keys through it.
63impl fmt::Debug for TypeKey {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        f.debug_tuple("TypeKey").field(&&*self.canon).finish()
66    }
67}
68
69/// Structured failure of [`TypeKey::parse`]: the offending input plus the
70/// underlying `syn` parse error.
71#[derive(Debug)]
72pub struct TypeKeyParseError {
73    pub input: String,
74    pub error: syn::Error,
75}
76
77impl fmt::Display for TypeKeyParseError {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "invalid type `{}`: {}", self.input, self.error)
80    }
81}
82
83impl std::error::Error for TypeKeyParseError {}
84
85impl TypeKey {
86    /// Build a key by parsing the input as a type and normalizing.
87    ///
88    /// The parse is kept for **validation** and then discarded: a key that
89    /// cannot be a type is a mistake worth reporting at the declaration, and a
90    /// key that can is still only its canonical string.
91    pub fn parse(s: &str) -> Result<Self, TypeKeyParseError> {
92        let ty: syn::Type = syn::parse_str(s).map_err(|error| TypeKeyParseError {
93            input: s.to_string(),
94            error,
95        })?;
96        Ok(Self::from_type(&ty))
97    }
98
99    /// Build a key directly from a `syn::Type` (normalizing a clone; the
100    /// input is not modified).
101    pub fn from_type(ty: &syn::Type) -> Self {
102        // Off the shared reduction, so this key and the model's type index
103        // cannot drift apart about what a type is called.
104        Self {
105            canon: crate::flat::canonical_type(ty)
106                .to_token_stream()
107                .to_string()
108                .into(),
109        }
110    }
111
112    /// Build a key for a bare item ident — infallible by construction (an
113    /// ident IS a single-segment path type; nothing to parse or normalize).
114    pub fn from_ident(ident: &syn::Ident) -> Self {
115        Self::from_type(&syn::parse_quote!(#ident))
116    }
117
118    /// The canonical string form.
119    pub fn as_str(&self) -> &str {
120        &self.canon
121    }
122
123    /// The bare item ident this key names — `Foo`, `a::Foo` → `Foo`,
124    /// `a::Foo<u8>::Bar` → `Bar`; `None` when the **last** segment carries
125    /// generic arguments (`Vec<u8>` names no bare item) or the key is not a
126    /// path.
127    /// Matches [`bare_path_ident`](crate::types_util::bare_path_ident) on the
128    /// same type — a correspondence this crate's tests pin.
129    ///
130    /// **A name is not syntax**, which is why this is the key's business and
131    /// producing a `syn::Type` is not. A caller that wants to look a declared
132    /// item up by name was never asking for tokens; it was asking the key what
133    /// it is called (#291).
134    pub fn ident(&self) -> Option<syn::Ident> {
135        let (ident, generic) = self.path_segments()?.pop()?;
136        // `bare_path_ident` reads `PathArguments` on the LAST segment only, so
137        // arguments earlier in the path do not disqualify the name.
138        if generic {
139            return None;
140        }
141        Some(ident)
142    }
143
144    /// The last path segment's ident, **ignoring** its generic arguments —
145    /// `Publisher<'static>` → `"Publisher"`, `a::Foo<u8>::Bar` → `"Bar"`.
146    /// `None` for anything that is not a path.
147    ///
148    /// The looser sibling of [`Self::ident`], for the callers that derive a
149    /// destination-language class name from a Rust type: a declaration writes
150    /// `ptr_class!(Publisher<'static>)` and means the class `Publisher`.
151    pub fn short_name(&self) -> Option<String> {
152        Some(self.path_segments()?.pop()?.0.to_string())
153    }
154
155    /// The **top-level** path segments of the canonical string: each segment's
156    /// ident, and whether that segment carried generic arguments. `None` if the
157    /// canon is not a path at all.
158    ///
159    /// # Read off the canonical string
160    ///
161    /// Deliberately, and not as a shortcut. `canon` is a token-stream
162    /// rendering, so its tokens are space-separated — `Vec < u8 >`, `& Foo`,
163    /// `a :: Foo` — and the structure is recoverable by tracking angle depth.
164    /// Reparsing the whole type instead would make a NAME depend on a
165    /// serialize-then-reparse round trip, which is the dependency #95 removed;
166    /// storing the derived names on the key would put derived state back on a
167    /// value whose whole point is that it carries none.
168    ///
169    /// **Nesting-aware, because a path segment is not the last thing before a
170    /// `<`.** `a::Foo<u8>::Bar` names `Bar`, and `Vec<a::B>` names `Vec` — the
171    /// `::` in the second belongs to the argument. Splitting at the first `<`
172    /// got the second right and the first wrong.
173    ///
174    /// `syn::parse_str::<syn::Ident>` on every segment is the totality check:
175    /// each non-path shape puts something in a segment that is not an ident —
176    /// `& Foo`, `[u8 ; 4]`, `( )`, `* const u8`, `dyn Error`, `fn () -> u8`.
177    fn path_segments(&self) -> Option<Vec<(syn::Ident, bool)>> {
178        let mut rest: &str = &self.canon;
179        // A qualified-self path renders its qualification first
180        // (`< T as Tr > :: Item`) and syn keeps only the tail in
181        // `path.segments` — so drop the group and read the rest as a plain path.
182        if rest.starts_with('<') {
183            rest = rest[close_angle(rest)? + 1..]
184                .trim_start()
185                .strip_prefix("::")?;
186        }
187
188        let bytes = rest.as_bytes();
189        let mut out = Vec::new();
190        let mut depth = 0usize;
191        let mut start = 0usize;
192        let mut ident_end: Option<usize> = None;
193        let mut i = 0usize;
194        while i < bytes.len() {
195            match bytes[i] {
196                b'<' => {
197                    if depth == 0 && ident_end.is_none() {
198                        ident_end = Some(i);
199                    }
200                    depth += 1;
201                }
202                // The `>` of a bare fn's `->` is an arrow, not a bracket, and
203                // miscounting it would let a `::` inside `Vec<fn() -> a::B>`
204                // read as a top-level separator.
205                b'>' if i > 0 && bytes[i - 1] == b'-' => {}
206                b'>' => depth = depth.saturating_sub(1),
207                b':' if depth == 0 && bytes.get(i + 1) == Some(&b':') => {
208                    out.push(segment(rest, start, ident_end, i)?);
209                    i += 2;
210                    start = i;
211                    ident_end = None;
212                    continue;
213                }
214                _ => {}
215            }
216            i += 1;
217        }
218        out.push(segment(rest, start, ident_end, bytes.len())?);
219        Some(out)
220    }
221}
222
223/// One path segment's ident (up to its own generic arguments, if any) and
224/// whether it had them. `None` when the text is not an ident, which is how a
225/// non-path canon is refused.
226fn segment(
227    s: &str,
228    start: usize,
229    ident_end: Option<usize>,
230    end: usize,
231) -> Option<(syn::Ident, bool)> {
232    let text = s[start..ident_end.unwrap_or(end)].trim();
233    Some((
234        syn::parse_str::<syn::Ident>(text).ok()?,
235        ident_end.is_some(),
236    ))
237}
238
239/// Byte index of the `>` closing the angle group `s` opens with.
240fn close_angle(s: &str) -> Option<usize> {
241    let bytes = s.as_bytes();
242    let mut depth = 0usize;
243    for (i, b) in bytes.iter().enumerate() {
244        match b {
245            b'<' => depth += 1,
246            b'>' if i > 0 && bytes[i - 1] == b'-' => {}
247            b'>' => {
248                depth = depth.saturating_sub(1);
249                if depth == 0 {
250                    return Some(i);
251                }
252            }
253            _ => {}
254        }
255    }
256    None
257}
258
259impl fmt::Display for TypeKey {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        f.write_str(&self.canon)
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use crate::types_util::bare_path_ident;
269
270    /// Every shape a key can hold, as a build script or a source could spell it.
271    ///
272    /// Generics on a **non-final** segment (`a::Foo<u8>::Bar`) and a `::` inside
273    /// an argument (`Vec<a::B>`) pull in opposite directions, and a `->` inside
274    /// an argument (`Vec<fn() -> a::B>`) breaks naive angle counting — each is a
275    /// way the string walk can be wrong while the easy cases still pass.
276    const SHAPES: &[&str] = &[
277        "Foo",
278        "a::Foo",
279        "a::b::Foo",
280        "std::string::String",
281        "Vec<u8>",
282        "Vec<a::B>",
283        "Vec<Vec<u8>>",
284        "a::Foo<u8>::Bar",
285        "Foo<u8>::Assoc",
286        "<T as Tr>::Item",
287        "Vec<fn() -> a::B>",
288        "Publisher<'static>",
289        "Option<Box<Node>>",
290        "&Foo",
291        "&mut Foo",
292        "&[u8]",
293        "[u8; 4]",
294        "()",
295        "(u8, u8)",
296        "(a::B, c::D)",
297        "*const u8",
298        "dyn Error",
299        "fn() -> u8",
300        "fn(u8) -> a::B",
301    ];
302
303    /// The accessors and the `syn` walks they replace answer identically.
304    ///
305    /// This is the whole warrant for reading names off the canonical string
306    /// instead of off a parsed type. Both walks are the incumbent definition —
307    /// `bare_path_ident` for [`TypeKey::ident`], and `rust_short_name_opt`'s
308    /// last-segment rule (spelled out here rather than imported, since it lives
309    /// under a language adapter) for [`TypeKey::short_name`].
310    #[test]
311    fn key_name_accessors_match_the_syn_walks() {
312        for spec in SHAPES {
313            let ty: syn::Type = syn::parse_str(spec).expect("test shape parses");
314            let key = TypeKey::from_type(&ty);
315
316            assert_eq!(
317                key.ident(),
318                bare_path_ident(&crate::flat::canonical_type(&ty)),
319                "ident() disagrees with bare_path_ident on `{spec}` (canon `{key}`)"
320            );
321
322            // `rust_short_name_opt`: the last path segment's ident, generic
323            // arguments and all.
324            let expected_short = match &crate::flat::canonical_type(&ty) {
325                syn::Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.to_string()),
326                _ => None,
327            };
328            assert_eq!(
329                key.short_name(),
330                expected_short,
331                "short_name() disagrees with the last-segment rule on `{spec}` (canon `{key}`)"
332            );
333        }
334    }
335
336    /// `short_name` is looser than `ident` in exactly one way: generic arguments
337    /// **on the last segment**.
338    #[test]
339    fn short_name_reads_through_last_segment_generics_and_ident_does_not() {
340        let key = TypeKey::from_type(&syn::parse_quote!(Publisher<'static>));
341        assert_eq!(key.short_name().as_deref(), Some("Publisher"));
342        assert_eq!(key.ident(), None);
343
344        // Arguments EARLIER in the path disqualify nothing: the segment being
345        // named is `Bar`, and it has none.
346        let nested = TypeKey::from_type(&syn::parse_quote!(a::Foo<u8>::Bar));
347        assert_eq!(nested.short_name().as_deref(), Some("Bar"));
348        assert_eq!(
349            nested.ident().map(|i| i.to_string()).as_deref(),
350            Some("Bar")
351        );
352    }
353
354    /// A qualified-self path names its tail, like `bare_path_ident` does —
355    /// syn keeps only `Item` in `path.segments`, and so does the string walk.
356    #[test]
357    fn qualified_self_paths_name_their_tail() {
358        let key = TypeKey::from_type(&syn::parse_quote!(<T as Tr>::Item));
359        assert_eq!(key.short_name().as_deref(), Some("Item"));
360        assert_eq!(key.ident().map(|i| i.to_string()).as_deref(), Some("Item"));
361    }
362
363    /// A `::` inside a generic argument is not a path separator, and neither
364    /// angle counting nor the `->` in a bare-fn argument may make it look like
365    /// one.
366    #[test]
367    fn separators_inside_generic_arguments_are_not_path_separators() {
368        for (spec, expected) in [
369            ("Vec<a::B>", Some("Vec")),
370            ("Vec<fn() -> a::B>", Some("Vec")),
371            ("Vec<Vec<a::B>>", Some("Vec")),
372        ] {
373            let key = TypeKey::from_type(&syn::parse_str(spec).expect("test shape"));
374            assert_eq!(key.short_name().as_deref(), expected, "on `{spec}`");
375        }
376    }
377
378    /// A name comes back out as the ident it names — `from_ident` is the inverse.
379    #[test]
380    fn ident_round_trips_through_from_ident() {
381        let ident = syn::Ident::new("ZKeyExpr", proc_macro2::Span::call_site());
382        let key = TypeKey::from_ident(&ident);
383        assert_eq!(key.ident().as_ref(), Some(&ident));
384        assert_eq!(key.short_name().as_deref(), Some("ZKeyExpr"));
385    }
386}