use super::{
code::KtCode,
model::*,
slot::KtPropertyValue,
types::{ImportSet, KtType},
};
pub const KOTLIN_BANNER: &str = "// Auto-generated by kotlin-codegen — do not edit by hand.";
pub(crate) const MAX_SIGNATURE_WIDTH: usize = 100;
impl KtFile {
pub fn render(&self) -> String {
let mut imports = ImportSet::new(&self.package);
for fqn in &self.extra_imports {
imports.register(fqn);
}
let mut raw_imports = Vec::new();
for d in &self.decls {
collect_decl_imports(d, &mut raw_imports);
}
for fqn in raw_imports {
imports.register(&fqn);
}
let mut body = String::new();
for (i, d) in self.decls.iter().enumerate() {
if i > 0 {
body.push('\n');
}
render_decl(d, 0, &mut imports, &mut body);
}
let mut out = String::new();
let banner_text = self.banner.as_deref().unwrap_or(KOTLIN_BANNER);
if !banner_text.is_empty() {
out.push_str(banner_text);
out.push('\n');
}
if !self.package.is_empty() {
out.push_str(&format!("package {}\n", self.package));
}
let import_lines = imports.import_lines();
if !import_lines.is_empty() {
out.push('\n');
for l in &import_lines {
out.push_str(l);
out.push('\n');
}
}
if !out.is_empty() {
out.push('\n');
}
out.push_str(&body);
if !out.ends_with('\n') {
out.push('\n');
}
out
}
}
fn render_code_inline(c: &KtCode) -> String {
let mut s = String::new();
c.render(0, &mut s);
s.trim_end().to_string()
}
fn collect_decl_imports(d: &KtDecl, sink: &mut Vec<String>) {
match d {
KtDecl::Class(c) => {
for p in c.ctor_params() {
if let Some(default) = &p.default {
default.collect_imports(sink);
}
}
for (_, args) in c.supertypes.iter() {
if let Some(args) = args {
args.collect_imports(sink);
}
}
for e in c.kind.entries() {
if let Some(args) = &e.args {
args.collect_imports(sink);
}
}
for m in &c.members {
collect_decl_imports(m, sink);
}
if let Some(comp) = &c.companion {
collect_companion_imports(comp, sink);
}
}
KtDecl::Fun(f) => collect_fun_imports(f, sink),
KtDecl::FunInterface(i) => collect_fun_sig_imports(&i.method, sink),
KtDecl::Property(p) => {
p.value.collect_imports(sink);
if let Some(a) = &p.accessors {
a.collect_imports(sink);
}
}
KtDecl::TypeAlias { .. } => {}
KtDecl::Raw { code, .. } => code.collect_imports(sink),
}
}
fn collect_companion_imports(c: &KtCompanion, sink: &mut Vec<String>) {
for (_, args) in c.supertypes.iter() {
if let Some(args) = args {
args.collect_imports(sink);
}
}
for m in &c.members {
collect_decl_imports(m, sink);
}
}
fn collect_fun_imports(f: &KtFun, sink: &mut Vec<String>) {
match &f.body {
KtBody::Expr(c) | KtBody::Block(c) => c.collect_imports(sink),
KtBody::None | KtBody::External => {}
}
collect_param_imports(&f.params, sink);
}
fn collect_fun_sig_imports(f: &KtFunSig, sink: &mut Vec<String>) {
collect_param_imports(&f.params, sink);
}
fn collect_param_imports(params: &[KtParam], sink: &mut Vec<String>) {
for p in params {
if let Some(default) = &p.default {
default.collect_imports(sink);
}
}
}
fn indent(level: usize, out: &mut String) {
for _ in 0..level {
out.push_str(" ");
}
}
fn render_kdoc(doc: &str, level: usize, out: &mut String) {
let lines: Vec<&str> = doc.lines().collect();
if lines.len() == 1 {
indent(level, out);
out.push_str(&format!("/** {} */\n", lines[0]));
return;
}
indent(level, out);
out.push_str("/**\n");
for l in &lines {
indent(level, out);
if l.is_empty() {
out.push_str(" *\n");
} else {
out.push_str(&format!(" * {l}\n"));
}
}
indent(level, out);
out.push_str(" */\n");
}
fn render_decl(d: &KtDecl, level: usize, imports: &mut ImportSet, out: &mut String) {
match d {
KtDecl::Class(c) => render_class(c, level, imports, out),
KtDecl::Fun(f) => render_fun(f, level, imports, out),
KtDecl::FunInterface(i) => render_fun_interface(i, level, imports, out),
KtDecl::Property(p) => render_property(p, level, imports, out),
KtDecl::TypeAlias { vis, name, target } => {
indent(level, out);
out.push_str(&format!(
"{}typealias {name} = {}\n",
vis.prefix(),
target.render(imports)
));
}
KtDecl::Raw { code, .. } => code.render(level, out),
}
}
fn render_ctor_param(p: &KtCtorParam, imports: &mut ImportSet) -> String {
let mut s = String::new();
for a in &p.annotations {
s.push_str(&format!("@{a} "));
}
s.push_str(p.vis.prefix());
if p.overrides {
s.push_str("override ");
}
match p.prop {
Some(false) => s.push_str("val "),
Some(true) => s.push_str("var "),
None => {}
}
s.push_str(&format!("{}: {}", p.name, p.ty.render(imports)));
if let Some(d) = &p.default {
s.push_str(&format!(" = {}", render_code_inline(d)));
}
s
}
fn render_supertypes(supertypes: &KtSupertypes, imports: &mut ImportSet, out: &mut String) {
if supertypes.is_empty() {
return;
}
let sts: Vec<String> = supertypes
.iter()
.map(|(ty, args)| {
let t = ty.render(imports);
match args {
Some(a) => format!("{t}({})", render_code_inline(a)),
None => t,
}
})
.collect();
out.push_str(&format!(" : {}", sts.join(", ")));
}
fn render_companion(c: &KtCompanion, level: usize, imports: &mut ImportSet, out: &mut String) {
if let Some(doc) = &c.kdoc {
render_kdoc(doc, level, out);
}
for a in &c.annotations {
indent(level, out);
out.push_str(&format!("@{a}\n"));
}
indent(level, out);
out.push_str(c.vis.prefix());
out.push_str("companion object");
if let Some(name) = &c.name {
out.push(' ');
out.push_str(name);
}
render_supertypes(&c.supertypes, imports, out);
if c.members.is_empty() {
out.push('\n');
return;
}
out.push_str(" {\n");
for (i, m) in c.members.iter().enumerate() {
if i > 0 {
out.push('\n');
}
render_decl(m, level + 1, imports, out);
}
indent(level, out);
out.push_str("}\n");
}
fn render_class(c: &KtClass, level: usize, imports: &mut ImportSet, out: &mut String) {
if let Some(doc) = &c.kdoc {
render_kdoc(doc, level, out);
}
let mut annotations = c.annotations.clone();
if matches!(c.kind, KtClassKind::Value { .. }) && !annotations.iter().any(|a| a == "JvmInline")
{
annotations.insert(0, "JvmInline".to_string());
}
for a in &annotations {
indent(level, out);
out.push_str(&format!("@{a}\n"));
}
indent(level, out);
out.push_str(c.vis.prefix());
out.push_str(c.kind.keyword());
out.push(' ');
out.push_str(&c.name);
let ctor_params = c.ctor_params();
if !ctor_params.is_empty() {
let ps: Vec<String> = ctor_params
.iter()
.map(|p| render_ctor_param(p, imports))
.collect();
out.push_str(&format!("({})", ps.join(", ")));
}
render_supertypes(&c.supertypes, imports, out);
let entries = c.kind.entries();
let has_body = !entries.is_empty() || !c.members.is_empty() || c.companion.is_some();
if !has_body {
out.push('\n');
return;
}
out.push_str(" {\n");
if !entries.is_empty() {
for (i, e) in entries.iter().enumerate() {
indent(level + 1, out);
out.push_str(&e.name);
if let Some(args) = &e.args {
out.push_str(&format!("({})", render_code_inline(args)));
}
out.push_str(if i + 1 == entries.len() { ";\n" } else { ",\n" });
}
if !c.members.is_empty() || c.companion.is_some() {
out.push('\n');
}
}
let mut first = true;
for m in &c.members {
if !first {
out.push('\n');
}
first = false;
render_decl(m, level + 1, imports, out);
}
if let Some(comp) = &c.companion {
if !first {
out.push('\n');
}
render_companion(comp, level + 1, imports, out);
}
indent(level, out);
out.push_str("}\n");
}
fn render_signature_param(p: &KtParam, imports: &mut ImportSet, level: usize) -> String {
let name_prefix = format!("{}: ", p.name);
let default = p
.default
.as_ref()
.map(|d| format!(" = {}", render_code_inline(d)))
.unwrap_or_default();
let type_col = level * 4 + name_prefix.len();
format!(
"{name_prefix}{}{default}",
render_type_wrapped(&p.ty, imports, level, type_col)
)
}
fn render_type_wrapped(
ty: &KtType,
imports: &mut ImportSet,
level: usize,
start_col: usize,
) -> String {
let single = ty.render(imports);
if start_col + single.len() <= MAX_SIGNATURE_WIDTH {
return single;
}
let KtType::Function {
params,
ret,
nullable,
} = ty
else {
return single;
};
if params.is_empty() {
return single;
}
let mut s = String::from(if *nullable { "((" } else { "(" });
s.push('\n');
for (name, pty) in params {
indent(level + 1, &mut s);
let prefix = if name.is_empty() {
String::new()
} else {
format!("{name}: ")
};
s.push_str(&prefix);
let col = (level + 1) * 4 + prefix.len();
s.push_str(&render_type_wrapped(pty, imports, level + 1, col));
s.push_str(",\n");
}
indent(level, &mut s);
let closer = ") -> ";
s.push_str(closer);
s.push_str(&render_type_wrapped(
ret,
imports,
level,
level * 4 + closer.len(),
));
if *nullable {
s.push_str(")?");
}
s
}
struct SigView<'a> {
kdoc: Option<&'a String>,
annotations: &'a [String],
vis: KtVis,
modifiers: &'a [String],
external: bool,
generics: &'a [String],
receiver: Option<&'a KtType>,
name: &'a str,
params: &'a [KtParam],
ret: Option<&'a KtType>,
}
impl<'a> From<&'a KtFun> for SigView<'a> {
fn from(f: &'a KtFun) -> Self {
SigView {
kdoc: f.kdoc.as_ref(),
annotations: &f.annotations,
vis: f.vis,
modifiers: &f.modifiers,
external: matches!(f.body, KtBody::External),
generics: &f.generics,
receiver: f.receiver.as_ref(),
name: &f.name,
params: &f.params,
ret: f.ret.as_ref(),
}
}
}
impl<'a> From<&'a KtFunSig> for SigView<'a> {
fn from(f: &'a KtFunSig) -> Self {
SigView {
kdoc: f.kdoc.as_ref(),
annotations: &f.annotations,
vis: f.vis,
modifiers: &[],
external: false,
generics: &f.generics,
receiver: f.receiver.as_ref(),
name: &f.name,
params: &f.params,
ret: f.ret.as_ref(),
}
}
}
fn render_fun_signature(f: &SigView<'_>, level: usize, imports: &mut ImportSet, out: &mut String) {
if let Some(doc) = f.kdoc {
render_kdoc(doc, level, out);
}
for a in f.annotations {
indent(level, out);
out.push_str(&format!("@{a}\n"));
}
indent(level, out);
out.push_str(f.vis.prefix());
if f.external {
out.push_str("external ");
}
for m in f.modifiers {
out.push_str(m);
out.push(' ');
}
out.push_str("fun ");
if !f.generics.is_empty() {
out.push_str(&format!("<{}> ", f.generics.join(", ")));
}
if let Some(recv) = f.receiver {
out.push_str(&recv.render_receiver(imports));
out.push('.');
}
out.push_str(f.name);
let ps: Vec<String> = f
.params
.iter()
.map(|p| {
let mut s = format!("{}: {}", p.name, p.ty.render(imports));
if let Some(d) = &p.default {
s.push_str(&format!(" = {}", render_code_inline(d)));
}
s
})
.collect();
let ret_suffix = match f.ret {
Some(rt) => {
let rendered = rt.render(imports);
if rendered != KtType::UNIT {
format!(": {rendered}")
} else {
String::new()
}
}
None => String::new(),
};
let header_col = out.rsplit('\n').next().map_or(out.len(), str::len);
let single_line = format!("({}){ret_suffix}", ps.join(", "));
if !ps.is_empty() && header_col + single_line.len() > MAX_SIGNATURE_WIDTH {
out.push_str("(\n");
for p in f.params {
indent(level + 1, out);
out.push_str(&render_signature_param(p, imports, level + 1));
out.push_str(",\n");
}
indent(level, out);
out.push(')');
out.push_str(&ret_suffix);
} else {
out.push_str(&single_line);
}
}
fn render_fun_sig(f: &KtFunSig, level: usize, imports: &mut ImportSet, out: &mut String) {
render_fun_signature(&f.into(), level, imports, out);
out.push('\n');
}
fn render_fun(f: &KtFun, level: usize, imports: &mut ImportSet, out: &mut String) {
render_fun_signature(&f.into(), level, imports, out);
match &f.body {
KtBody::None | KtBody::External => out.push('\n'),
KtBody::Expr(c) => {
let rendered = render_code_inline(c);
if rendered.lines().count() <= 1 {
out.push_str(&format!(" = {rendered}\n"));
} else {
out.push_str(" =\n");
for line in rendered.lines() {
indent(level + 1, out);
out.push_str(line);
out.push('\n');
}
}
}
KtBody::Block(c) => {
out.push_str(" {\n");
c.render(level + 1, out);
indent(level, out);
out.push_str("}\n");
}
}
}
fn render_fun_interface(
i: &KtFunInterface,
level: usize,
imports: &mut ImportSet,
out: &mut String,
) {
if let Some(doc) = &i.kdoc {
render_kdoc(doc, level, out);
}
indent(level, out);
out.push_str(i.vis.prefix());
out.push_str("fun interface ");
out.push_str(&i.name);
if !i.type_params.is_empty() {
out.push_str(&format!("<{}>", i.type_params.join(", ")));
}
out.push_str(" {\n");
render_fun_sig(&i.method, level + 1, imports, out);
indent(level, out);
out.push_str("}\n");
}
fn render_property(p: &KtProperty, level: usize, imports: &mut ImportSet, out: &mut String) {
if let Some(doc) = &p.kdoc {
render_kdoc(doc, level, out);
}
indent(level, out);
for a in &p.annotations {
out.push_str(&format!("@{a} "));
}
out.push_str(p.vis.prefix());
for m in &p.modifiers {
out.push_str(m);
out.push(' ');
}
out.push_str(if p.mutable { "var " } else { "val " });
out.push_str(&p.name);
if let Some(ty) = &p.ty {
out.push_str(&format!(": {}", ty.render(imports)));
}
match &p.value {
KtPropertyValue::None => {}
KtPropertyValue::Delegate(d) => {
out.push_str(&format!(" by {}", render_code_inline(d)));
}
KtPropertyValue::Initializer(i) => {
out.push_str(&format!(" = {}", render_code_inline(i)));
}
}
out.push('\n');
if let Some(acc) = &p.accessors {
acc.render(level + 1, out);
}
}
#[cfg(test)]
pub(crate) fn render_one(d: &KtDecl, package: &str) -> String {
KtFile::new(package).decl(d.clone()).render()
}