Skip to main content

invalid/
invalid.rs

1//! What a *broken* model looks like, and what the validator says about it.
2//!
3//! ```text
4//! cargo run --example invalid
5//! ```
6//!
7//! Two kinds of mistake are shown, and they are caught at different moments:
8//!
9//! * **Shapes the model refuses to build.** These never reach validation —
10//!   the builder panics, or the type simply has nowhere to put the bad value.
11//!   The last section demonstrates a few by catching the panics.
12//! * **Everything else**, reported by [`KtFile::validate`] as a list of
13//!   diagnostics with a scope path locating each one.
14//!
15//! The output is checked against `tests/golden/invalid.txt` by
16//! `tests/examples.rs`.
17
18use kotlin_codegen::{
19    merge_files, Check, KtClass, KtClassModifier, KtCode, KtCompanion, KtCtorParam, KtDecl,
20    KtEnumEntry, KtFile, KtFun, KtParam, KtProperty, KtType, KtVis, ValidationPolicy,
21};
22
23fn main() {
24    println!("=== diagnostics (default: every check is an error) ===");
25    for file in broken_files() {
26        for d in file.validate() {
27            println!("{d}");
28        }
29    }
30
31    println!("\n=== merging refuses to produce anything ===");
32    match merge_files(broken_files()) {
33        Ok(_) => println!("unexpectedly merged"),
34        // The Display of the error is what a build script would surface.
35        Err(e) => print!("{e}"),
36    }
37
38    println!("\n=== the same model under `warn_all()` ===");
39    let (files, warnings) = merge_files_warning();
40    println!(
41        "merged {} file(s) with {} warning(s); generation continues",
42        files.len(),
43        warnings.len()
44    );
45
46    println!("\n=== one check downgraded, another switched off ===");
47    let policy = ValidationPolicy::new()
48        .warn(Check::DuplicateFunction)
49        .allow(Check::InvalidIdentifier);
50    for file in broken_files() {
51        for d in file.validate_with(&policy) {
52            println!("{d}");
53        }
54    }
55
56    println!("\n=== shapes the model will not build at all ===");
57    for (what, outcome) in refused_shapes() {
58        println!("{what}\n    -> {outcome}");
59    }
60}
61
62/// Between them, these files trigger every check the validator can report.
63fn broken_files() -> Vec<KtFile> {
64    vec![
65        broken_declarations(),
66        // A package path that is not a dotted sequence of legal identifiers.
67        KtFile::new("io..example.object").decl(KtFun::new("stillChecked").body(KtCode::new())),
68    ]
69}
70
71fn broken_declarations() -> KtFile {
72    KtFile::new("io.example.broken")
73        // Two classes of the same name: a redeclaration in the type namespace.
74        .decl(KtClass::class_("Session"))
75        .decl(KtClass::class_("Session"))
76        // An interface and a type alias are both classifier declarations, so they collide.
77        .decl(
78            KtClass::interface_("Describable")
79                .member(KtFun::new("describe").returns(KtType::string())),
80        )
81        .decl(KtDecl::TypeAlias {
82            vis: KtVis::Public,
83            name: "Describable".to_string(),
84            target: KtType::string(),
85        })
86        // Same name AND same parameter types: not an overload.
87        .decl(
88            KtFun::new("send")
89                .param(KtParam::new("value", KtType::int()))
90                .returns(KtType::boolean())
91                .body(KtCode::new()),
92        )
93        .decl(
94            KtFun::new("send")
95                .param(KtParam::new("other", KtType::int()))
96                .returns(KtType::long())
97                .body(KtCode::new()),
98        )
99        // Extensions are keyed on their receiver, so these two collide while
100        // the same pair on different receivers would not.
101        .decl(
102            KtFun::new("asRaw")
103                .receiver(KtType::cls("io.example.Codec"))
104                .body(KtCode::new()),
105        )
106        .decl(
107            KtFun::new("asRaw")
108                .receiver(KtType::cls("io.example.Codec"))
109                .body(KtCode::new()),
110        )
111        // ...as here: same name, different receiver, no diagnostic.
112        .decl(
113            KtFun::new("asRaw")
114                .receiver(KtType::cls("io.example.Other"))
115                .body(KtCode::new()),
116        )
117        // Names that are not legal Kotlin identifiers.
118        .decl(
119            KtClass::class_("My-Class")
120                .ctor_param(KtCtorParam::new("2fast", KtType::int()))
121                .member(
122                    KtFun::new("object")
123                        .param(KtParam::new("in", KtType::int()))
124                        .body(KtCode::new()),
125                ),
126        )
127        // `val x` with no type, no value and no accessors.
128        .decl(KtProperty::val("bare"))
129        // A function with no body, at top level, where that cannot mean
130        // "abstract".
131        .decl(KtFun::new("nobody"))
132        // An abstract class member missing the `abstract` keyword.
133        .decl(
134            KtClass::class_with(KtClassModifier::Abstract, "Base")
135                .member(KtFun::new("unimplemented")),
136        )
137        // An enum whose entries do not call the constructor it declares.
138        .decl(
139            KtClass::enum_("Priority")
140                .ctor_param(KtCtorParam::new("code", KtType::int()).val())
141                .entry(KtEnumEntry::with_args("HIGH", "1"))
142                .entry(KtEnumEntry::new("LOW")),
143        )
144        // A constructor property and a member property of one name: both live
145        // in the value namespace.
146        .decl(
147            KtClass::class_("Holder")
148                .ctor_param(KtCtorParam::new("id", KtType::long()).val())
149                .member(KtProperty::val("id").initializer("0")),
150        )
151        // A named companion object colliding with a nested class.
152        .decl(
153            KtClass::class_("Outer")
154                .member(KtClass::class_("Factory"))
155                .companion(KtCompanion::named("Factory")),
156        )
157        // Two raw blocks claiming one identity with different bodies.
158        .decl(KtDecl::Raw {
159            name: "__loader".to_string(),
160            code: KtCode::new().line("internal val __loader = 1"),
161        })
162        .decl(KtDecl::Raw {
163            name: "__loader".to_string(),
164            code: KtCode::new().line("internal val __loader = 2"),
165        })
166        // Two different classes referenced from raw text under one short name:
167        // the text already says `Codec`, so it cannot mean both.
168        .import("io.example.a.Codec")
169        .import("io.example.b.Codec")
170}
171
172/// The same model, adopted the way a generator that already produces output
173/// would: every check a warning, so nothing is blocked while the backlog is
174/// worked through.
175fn merge_files_warning() -> (Vec<KtFile>, Vec<kotlin_codegen::Diagnostic>) {
176    kotlin_codegen::merge_files_with(broken_files(), &ValidationPolicy::warn_all())
177        .expect("warnings never stop generation")
178}
179
180/// Shapes that are not merely *detected* — they cannot be constructed. The
181/// builder rejects them, so no invalid value ever reaches the renderer.
182fn refused_shapes() -> Vec<(&'static str, String)> {
183    vec![
184        (
185            "object Foo(x: Int)  — an object has no primary constructor",
186            refused(|| {
187                KtClass::object_("Foo").ctor_param(KtCtorParam::new("x", KtType::int()));
188            }),
189        ),
190        (
191            "data class Foo(x: Int)  — a data class parameter must be a property",
192            refused(|| {
193                KtClass::data("Foo", KtCtorParam::new("x", KtType::int()));
194            }),
195        ),
196        (
197            "value class Foo(var x: Long)  — a value class wraps one read-only property",
198            refused(|| {
199                KtClass::value("Foo", KtCtorParam::new("x", KtType::long()).var());
200            }),
201        ),
202        (
203            "class A : B(x), C(y)  — Kotlin constructs at most one superclass",
204            refused(|| {
205                KtClass::class_("A")
206                    .extends(KtType::cls("B"), Some("x"))
207                    .extends(KtType::cls("C"), Some("y"));
208            }),
209        ),
210        (
211            "external fun f() { … }  — `external` is a body kind, not a modifier",
212            refused(|| {
213                KtFun::new("f").modifier("external");
214            }),
215        ),
216        (
217            "enum entry on a non-enum class",
218            refused(|| {
219                KtClass::class_("C").entry(KtEnumEntry::new("A"));
220            }),
221        ),
222        (
223            "companion object with an empty name",
224            refused(|| {
225                KtCompanion::named("");
226            }),
227        ),
228    ]
229}
230
231/// Run a builder that is expected to panic and return its message, so the
232/// example can show what the author would see.
233fn refused(f: impl FnOnce() + std::panic::UnwindSafe) -> String {
234    // The panic is the point here, so keep the default hook from printing a
235    // backtrace over the example's output.
236    let hook = std::panic::take_hook();
237    std::panic::set_hook(Box::new(|_| {}));
238    let result = std::panic::catch_unwind(f);
239    std::panic::set_hook(hook);
240    match result {
241        Ok(()) => "built successfully (unexpected!)".to_string(),
242        Err(payload) => payload
243            .downcast_ref::<String>()
244            .cloned()
245            .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
246            .unwrap_or_else(|| "panicked".to_string()),
247    }
248}