pleme-doc-gen 0.1.41

Rust replacement for the M0 Python _gen-patterns.py + _gen-docs.py scripts in pleme-io/actions. Walks every action.yml + emits substrate's patterns-full.nix + per-action README.md + root catalog. Per the NO-SHELL prime directive.
//! Typed Kotlin AST + pretty-printer (Gradle-KTS DSL subset).
//!
//! Per the ★★★ NO-format!()-for-code prime directive. Covers the
//! manifest subset of Kotlin used in `build.gradle.kts` and
//! `settings.gradle.kts`:
//!
//!   plugins { application }
//!   group = "io.example"
//!   version = "1.0"
//!   repositories { mavenCentral() }
//!   dependencies {
//!     implementation("g:n:v")
//!   }
//!   java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
//!
//! NOT a general Kotlin AST — no control flow, no class bodies, no
//! lambdas with parameters. Adding shape (lambda params, when-expressions,
//! property delegates) means extending an enum variant, never punching
//! format!() through.

use std::fmt::Write;

/// Typed Kotlin expression — covers the RHS shapes in Gradle KTS.
#[derive(Debug, Clone)]
pub enum Expr {
    /// `"text"` — double-quoted with Kotlin's escape rules.
    String(String),
    /// `42` — integer literal.
    Int(i64),
    /// Bare identifier: `application`, `mavenCentral`, `JavaLanguageVersion`.
    Ident(String),
    /// `` `java-library` `` — backtick-escaped identifier for plugin
    /// IDs that contain Kotlin-illegal characters.
    BacktickIdent(String),
    /// `name(args, …)` or `receiver.name(args, …)`. `receiver: None`
    /// renders the bare call.
    Call { receiver: Option<Box<Expr>>, name: String, args: Vec<Expr> },
}

impl Expr {
    pub fn s(v: impl Into<String>) -> Self { Self::String(v.into()) }
    pub fn i(v: i64) -> Self { Self::Int(v) }
    pub fn ident(v: impl Into<String>) -> Self { Self::Ident(v.into()) }
    pub fn backtick(v: impl Into<String>) -> Self { Self::BacktickIdent(v.into()) }
    pub fn call(name: impl Into<String>, args: impl IntoIterator<Item = Expr>) -> Self {
        Self::Call { receiver: None, name: name.into(), args: args.into_iter().collect() }
    }
    pub fn method(receiver: Expr, name: impl Into<String>, args: impl IntoIterator<Item = Expr>) -> Self {
        Self::Call {
            receiver: Some(Box::new(receiver)),
            name: name.into(),
            args: args.into_iter().collect(),
        }
    }

    fn render(&self) -> String {
        match self {
            Self::String(s) => render_kotlin_string(s),
            Self::Int(n) => n.to_string(),
            Self::Ident(s) => s.clone(),
            Self::BacktickIdent(s) => {
                let mut out = String::with_capacity(s.len() + 2);
                out.push('`');
                out.push_str(s);
                out.push('`');
                out
            }
            Self::Call { receiver, name, args } => {
                let mut out = String::new();
                if let Some(r) = receiver {
                    out.push_str(&r.render());
                    out.push('.');
                }
                out.push_str(name);
                out.push('(');
                let parts: Vec<String> = args.iter().map(Expr::render).collect();
                out.push_str(&parts.join(", "));
                out.push(')');
                out
            }
        }
    }
}

/// Kotlin double-quoted string escape — covers `"`, `\`, `$` (which is
/// interpolation-significant), tab, newline, CR.
fn render_kotlin_string(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    out.push('"');
    for c in s.chars() {
        match c {
            '\\' => out.push_str(r"\\"),
            '"' => out.push_str("\\\""),
            '$' => out.push_str("\\$"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c => out.push(c),
        }
    }
    out.push('"');
    out
}

/// A typed top-level statement.
#[derive(Debug, Clone)]
pub enum Stmt {
    /// `name = expr` — property assignment.
    Assign { name: String, value: Expr },
    /// Bare expression statement: `mavenCentral()` / `implementation("g:n:v")`.
    Expr(Expr),
    /// Named block: `name { body }`. Single-line when `inline = true`
    /// (e.g. `plugins { application }`), multi-line otherwise.
    Block { name: String, body: Vec<Stmt>, inline: bool },
    /// Blank line between stanzas.
    Blank,
    /// `// comment` line.
    Comment(String),
}

/// A typed Kotlin source file — `build.gradle.kts` / `settings.gradle.kts`.
#[derive(Debug, Clone, Default)]
pub struct File {
    pub stmts: Vec<Stmt>,
}

impl File {
    pub fn new() -> Self { Self::default() }
    pub fn push(&mut self, s: Stmt) -> &mut Self { self.stmts.push(s); self }
    pub fn render(&self) -> String {
        let mut out = String::new();
        render_at(&self.stmts, 0, &mut out);
        out
    }
}

/// A statement is "inlinable" — fits on one line inside `name { … }` —
/// only when it's a bare expression or a flat property assignment.
/// Nested Blocks force the outer block into multi-line layout.
fn is_inlinable(stmt: &Stmt) -> bool {
    matches!(stmt, Stmt::Expr(_) | Stmt::Assign { .. })
}

fn render_inline_part(stmt: &Stmt) -> Option<String> {
    match stmt {
        Stmt::Expr(e) => Some(e.render()),
        Stmt::Assign { name, value } => {
            let mut a = String::from(name);
            a.push_str(" = ");
            a.push_str(&value.render());
            Some(a)
        }
        _ => None,
    }
}

fn render_at(stmts: &[Stmt], depth: usize, out: &mut String) {
    let pad = "    ".repeat(depth);
    for s in stmts {
        match s {
            Stmt::Assign { name, value } => {
                let _ = writeln!(out, "{pad}{name} = {}", value.render());
            }
            Stmt::Expr(e) => {
                let _ = writeln!(out, "{pad}{}", e.render());
            }
            Stmt::Block { name, body, inline: true }
                if body.len() <= 2 && body.iter().all(is_inlinable) =>
            {
                // Inline form: `name { item1; item2 }` — single line.
                // For Gradle KTS the canonical form puts a single item or
                // two items separated by spaces inside `{ … }`.
                let parts: Vec<String> = body.iter().filter_map(render_inline_part).collect();
                let _ = writeln!(out, "{pad}{name} {{ {} }}", parts.join("; "));
            }
            Stmt::Block { name, body, .. } => {
                let _ = writeln!(out, "{pad}{name} {{");
                render_at(body, depth + 1, out);
                let _ = writeln!(out, "{pad}}}");
            }
            Stmt::Blank => out.push('\n'),
            Stmt::Comment(c) => {
                let _ = writeln!(out, "{pad}// {c}");
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn string_escapes_dollar_for_interp_safety() {
        // `$foo` would interpolate inside double-quoted Kotlin strings;
        // our typed renderer guarantees raw `$` becomes `\$`.
        assert_eq!(render_kotlin_string("$home"), r#""\$home""#);
    }
    #[test]
    fn backtick_ident_wraps_with_backticks() {
        assert_eq!(Expr::backtick("java-library").render(), "`java-library`");
    }
    #[test]
    fn method_chain_renders_with_dot() {
        let e = Expr::method(Expr::ident("JavaLanguageVersion"), "of", vec![Expr::i(17)]);
        assert_eq!(e.render(), "JavaLanguageVersion.of(17)");
    }
    #[test]
    fn inline_block_collapses_short_body() {
        let mut f = File::new();
        f.push(Stmt::Block {
            name: "plugins".into(),
            body: vec![Stmt::Expr(Expr::backtick("java-library"))],
            inline: true,
        });
        let out = f.render();
        assert_eq!(out, "plugins { `java-library` }\n");
    }
    #[test]
    fn multi_line_block_indents_4spaces() {
        let mut f = File::new();
        f.push(Stmt::Block {
            name: "dependencies".into(),
            body: vec![
                Stmt::Expr(Expr::call("implementation", vec![Expr::s("g:n:v")])),
                Stmt::Expr(Expr::call("testImplementation", vec![Expr::s("g2:n2:v2")])),
            ],
            inline: false,
        });
        let out = f.render();
        assert!(out.starts_with("dependencies {\n"));
        assert!(out.contains("    implementation(\"g:n:v\")\n"));
        assert!(out.contains("    testImplementation(\"g2:n2:v2\")\n"));
        assert!(out.ends_with("}\n"));
    }
}