Skip to main content

kotlin_codegen/
types.rs

1//! Kotlin type references ([`KtType`]) and the per-file import collector
2//! ([`ImportSet`]).
3//!
4//! A type renders against an `ImportSet`: a dotted FQN registers an import
5//! and renders as its short name; a dot-free name renders bare (builtins,
6//! type variables). If two distinct FQNs share a simple name within one
7//! file, the first registered owns the import and any later one renders
8//! fully qualified — the file always compiles.
9
10use std::collections::BTreeMap;
11
12/// A Kotlin type reference.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub enum KtType {
15    /// A named type: builtin (`Int`), type variable (`R`), or class FQN
16    /// (`io.zenoh.jni.ZKeyExpr`), optionally generic (`List<T>`).
17    Named {
18        fqn: String,
19        args: Vec<KtType>,
20        nullable: bool,
21    },
22    /// A function type with **named** parameters:
23    /// `(je: String?, message: String) -> Unit`.
24    Function {
25        params: Vec<(String, KtType)>,
26        ret: Box<KtType>,
27        nullable: bool,
28    },
29}
30
31impl KtType {
32    pub const UNIT: &'static str = "Unit";
33
34    /// A named type (builtin, type variable, or class FQN).
35    pub fn cls(fqn: impl Into<String>) -> Self {
36        KtType::Named {
37            fqn: fqn.into(),
38            args: vec![],
39            nullable: false,
40        }
41    }
42
43    /// A generic named type, e.g. `generic("List", [cls("io.x.Y")])`.
44    pub fn generic(fqn: impl Into<String>, args: impl IntoIterator<Item = KtType>) -> Self {
45        KtType::Named {
46            fqn: fqn.into(),
47            args: args.into_iter().collect(),
48            nullable: false,
49        }
50    }
51
52    /// A function type with named parameters.
53    pub fn lambda(params: impl IntoIterator<Item = (String, KtType)>, ret: KtType) -> Self {
54        KtType::Function {
55            params: params.into_iter().collect(),
56            ret: Box::new(ret),
57            nullable: false,
58        }
59    }
60
61    pub fn unit() -> Self {
62        Self::cls("Unit")
63    }
64    pub fn int() -> Self {
65        Self::cls("Int")
66    }
67    pub fn long() -> Self {
68        Self::cls("Long")
69    }
70    pub fn boolean() -> Self {
71        Self::cls("Boolean")
72    }
73    pub fn string() -> Self {
74        Self::cls("String")
75    }
76    pub fn byte_array() -> Self {
77        Self::cls("ByteArray")
78    }
79    pub fn any() -> Self {
80        Self::cls("Any")
81    }
82    /// A bare type variable (`R`, `A`) — renders verbatim, never imported.
83    pub fn var_(name: impl Into<String>) -> Self {
84        Self::cls(name)
85    }
86    /// Shorthand for the ubiquitous `R` type variable.
87    pub fn var_r() -> Self {
88        Self::cls("R")
89    }
90
91    /// This type made nullable (`T?`).
92    pub fn nullable(mut self) -> Self {
93        match &mut self {
94            KtType::Named { nullable, .. } | KtType::Function { nullable, .. } => *nullable = true,
95        }
96        self
97    }
98
99    /// Whether this type is nullable (`T?` / `((…) -> …)?`).
100    pub fn is_nullable(&self) -> bool {
101        match self {
102            KtType::Named { nullable, .. } | KtType::Function { nullable, .. } => *nullable,
103        }
104    }
105
106    /// The (possibly dotted) name of a non-generic named type — `None` for
107    /// function types and generics. This is the FQN-or-short-name string a
108    /// leaf was constructed from.
109    pub fn leaf_name(&self) -> Option<&str> {
110        match self {
111            KtType::Named { fqn, args, .. } if args.is_empty() => Some(fqn),
112            _ => None,
113        }
114    }
115
116    /// The simple (dot-free) name of a named type: last FQN segment, generic
117    /// arguments ignored. `None` for function types.
118    pub fn simple_name(&self) -> Option<&str> {
119        match self {
120            KtType::Named { fqn, .. } => Some(fqn.rsplit('.').next().unwrap_or(fqn)),
121            KtType::Function { .. } => None,
122        }
123    }
124
125    /// Render in **extension-receiver** position — `fun <this>.name()`.
126    ///
127    /// A function type needs parentheses there, or the `.` binds to its return
128    /// type instead: `fun ((Int) -> String).ext()`, never
129    /// `fun (Int) -> String.ext()`. A nullable one is already parenthesized by
130    /// [`Self::render`], so it is left alone.
131    ///
132    /// ```
133    /// use kotlin_codegen::{ImportSet, KtType};
134    /// let mut imports = ImportSet::new("io.p");
135    /// let f = KtType::lambda([("x".to_string(), KtType::int())], KtType::string());
136    /// assert_eq!(f.render_receiver(&mut imports), "((x: Int) -> String)");
137    /// assert_eq!(KtType::string().render_receiver(&mut imports), "String");
138    /// ```
139    pub fn render_receiver(&self, imports: &mut ImportSet) -> String {
140        let rendered = self.render(imports);
141        if self.needs_receiver_parens() {
142            format!("({rendered})")
143        } else {
144            rendered
145        }
146    }
147
148    /// Whether writing this type in receiver position needs parentheses added
149    /// around it. True only for a non-nullable function type — a nullable one
150    /// is already parenthesized by [`Self::render`] and by its `Display`.
151    pub(crate) fn needs_receiver_parens(&self) -> bool {
152        matches!(
153            self,
154            KtType::Function {
155                nullable: false,
156                ..
157            }
158        )
159    }
160
161    /// Render to Kotlin source, registering imports in `imports`.
162    pub fn render(&self, imports: &mut ImportSet) -> String {
163        match self {
164            KtType::Named {
165                fqn,
166                args,
167                nullable,
168            } => {
169                let mut s = imports.short(fqn);
170                if !args.is_empty() {
171                    s.push('<');
172                    let rendered: Vec<String> = args.iter().map(|a| a.render(imports)).collect();
173                    s.push_str(&rendered.join(", "));
174                    s.push('>');
175                }
176                if *nullable {
177                    s.push('?');
178                }
179                s
180            }
181            KtType::Function {
182                params,
183                ret,
184                nullable,
185            } => {
186                let ps: Vec<String> = params
187                    .iter()
188                    .map(|(n, t)| {
189                        if n.is_empty() {
190                            t.render(imports)
191                        } else {
192                            format!("{n}: {}", t.render(imports))
193                        }
194                    })
195                    .collect();
196                let core = format!("({}) -> {}", ps.join(", "), ret.render(imports));
197                if *nullable {
198                    format!("({core})?")
199                } else {
200                    core
201                }
202            }
203        }
204    }
205}
206
207/// Renders the type with names exactly as constructed (FQNs stay fully
208/// qualified — no import shortening). For diagnostics and any context
209/// without an [`ImportSet`].
210impl std::fmt::Display for KtType {
211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212        match self {
213            KtType::Named {
214                fqn,
215                args,
216                nullable,
217            } => {
218                f.write_str(fqn)?;
219                if !args.is_empty() {
220                    write!(f, "<")?;
221                    for (i, a) in args.iter().enumerate() {
222                        if i > 0 {
223                            write!(f, ", ")?;
224                        }
225                        write!(f, "{a}")?;
226                    }
227                    write!(f, ">")?;
228                }
229                if *nullable {
230                    write!(f, "?")?;
231                }
232                Ok(())
233            }
234            KtType::Function {
235                params,
236                ret,
237                nullable,
238            } => {
239                if *nullable {
240                    write!(f, "(")?;
241                }
242                write!(f, "(")?;
243                for (i, (n, t)) in params.iter().enumerate() {
244                    if i > 0 {
245                        write!(f, ", ")?;
246                    }
247                    if !n.is_empty() {
248                        write!(f, "{n}: ")?;
249                    }
250                    write!(f, "{t}")?;
251                }
252                write!(f, ") -> {ret}")?;
253                if *nullable {
254                    write!(f, ")?")?;
255                }
256                Ok(())
257            }
258        }
259    }
260}
261
262/// Per-file import collector. Maps simple name → owning FQN; first
263/// registration wins, later distinct FQNs with the same simple name render
264/// fully qualified.
265#[derive(Default, Debug)]
266pub struct ImportSet {
267    /// The package of the file being rendered — same-package FQNs need no
268    /// import and render short.
269    package: String,
270    /// simple name → FQN that owns it in this file.
271    by_simple: BTreeMap<String, String>,
272    /// Top-level FUNCTION imports (lowercase simple names): Kotlin allows
273    /// several with the same simple name (overload resolution), so they
274    /// bypass the simple-name ownership map — every registered FQN gets its
275    /// import line.
276    fn_imports: std::collections::BTreeSet<String>,
277}
278
279impl ImportSet {
280    pub fn new(package: impl Into<String>) -> Self {
281        Self {
282            package: package.into(),
283            by_simple: BTreeMap::new(),
284            fn_imports: Default::default(),
285        }
286    }
287
288    /// Resolve a (possibly dotted) name to the text the use site should
289    /// emit, registering an import when needed. Only a dotted **identifier
290    /// path** (`io.zenoh.jni.ZKeyExpr`) is treated as an FQN; any other
291    /// shape (dot-free names, verbatim function-type strings) renders
292    /// unchanged.
293    pub fn short(&mut self, name: &str) -> String {
294        let is_fqn_path = name.contains('.')
295            && name
296                .chars()
297                .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.');
298        if !is_fqn_path {
299            return name.to_string();
300        }
301        let Some((_pkg, simple)) = name.rsplit_once('.') else {
302            return name.to_string();
303        };
304        // Lowercase simple name = a top-level function import (extension
305        // adapters, helpers): same-named overloads from different packages
306        // may coexist — register them all, always render short.
307        if simple.chars().next().is_some_and(|c| c.is_lowercase()) {
308            self.fn_imports.insert(name.to_string());
309            return simple.to_string();
310        }
311        match self.by_simple.get(simple) {
312            Some(owner) if owner == name => simple.to_string(),
313            Some(_) => name.to_string(), // collision: render fully qualified
314            None => {
315                self.by_simple.insert(simple.to_string(), name.to_string());
316                simple.to_string()
317            }
318        }
319    }
320
321    /// Register an FQN referenced only inside raw code text (so the import
322    /// line is emitted even though no `KtType` renders it).
323    pub fn register(&mut self, fqn: &str) {
324        let _ = self.short(fqn);
325    }
326
327    /// The sorted import lines for the file: every registered FQN except
328    /// same-package ones.
329    pub fn import_lines(&self) -> Vec<String> {
330        self.by_simple
331            .values()
332            .chain(self.fn_imports.iter())
333            .filter(|fqn| {
334                fqn.rsplit_once('.')
335                    .map(|(pkg, _)| pkg != self.package)
336                    .unwrap_or(false)
337            })
338            .map(|fqn| format!("import {fqn}"))
339            .collect::<std::collections::BTreeSet<_>>()
340            .into_iter()
341            .collect()
342    }
343}