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}