Skip to main content

zerodds_lang_render/
csharp.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Rendering the C# CST. Spec §5.
4//!
5//! One style, the .NET convention: four spaces, the opening brace on a
6//! line of its own — which is where C# puts it and Java does not — one
7//! member per line, and a blank line before anything with a body.
8//!
9//! Two things this renderer does that the Java one has no need for:
10//!
11//! - A [`Path`] that is rooted renders with `global::`. That prefix is
12//!   what makes a generated reference immune to capture by a name in
13//!   scope, and the tree is where the decision to root it was made.
14//! - A documentation comment renders as a `///` XML block, so the text
15//!   is escaped for XML. A member comment saying `sequence<long>` would
16//!   otherwise be a malformed element the moment anyone compiles with
17//!   `/doc`.
18
19use crate::writer::{Writer, comment_lines, escape_char, escape_string, verbatim_lines};
20use alloc::string::{String, ToString};
21use alloc::vec::Vec;
22use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
23use zerodds_lang_cst::csharp::{
24    Accessor, Attribute, AttributeArg, Block, Class, Constructor, Delegate, Enum, EnumMember, Expr,
25    Field, File, Interface, Item, Member, Method, Modifier, Namespace, Param, Property, Record,
26    Statement, Struct, SwitchSection, Type, TypeConstraint, TypeDecl, TypeParam, Using,
27};
28
29/// Render a whole file.
30#[must_use]
31pub fn file(file: &File) -> String {
32    let mut w = Writer::spaces4();
33    line_comment(&mut w, &file.doc);
34    if !file.doc.is_empty() {
35        w.blank_line();
36    }
37    for using in &file.usings {
38        render_using(&mut w, using);
39    }
40    if !file.usings.is_empty() {
41        w.blank_line();
42    }
43    render_items(&mut w, &file.items);
44    w.finish()
45}
46
47/// Render one type declaration, for a caller that has only one.
48#[must_use]
49pub fn type_decl(node: &TypeDecl) -> String {
50    let mut w = Writer::spaces4();
51    render_type_decl(&mut w, node);
52    w.finish()
53}
54
55/// Render one type, for a caller that has only one.
56#[must_use]
57pub fn type_(node: &Type) -> String {
58    type_text(node)
59}
60
61/// Render one expression, for a caller that has only one.
62#[must_use]
63pub fn expr(node: &Expr) -> String {
64    expr_text(node)
65}
66
67/// zerodds-lint: recursion-depth 64 (one level per IDL module; the
68/// module chain is cut at `MAX_MODULE_DEPTH` in
69/// `lang-rules/src/adapter/csharp.rs`)
70fn render_items(w: &mut Writer, items: &[Item]) {
71    for (index, item) in items.iter().enumerate() {
72        if index > 0 && !matches!(item, Item::Comment(_)) {
73            w.blank_line();
74        }
75        match item {
76            Item::Namespace(node) => render_namespace(w, node),
77            Item::Type(node) => render_type_decl(w, node),
78            Item::Comment(node) => line_comment(w, node),
79            Item::Verbatim(text) => verbatim_lines(w, text),
80        }
81    }
82}
83
84/// zerodds-lint: recursion-depth 64 (one level per IDL module; the
85/// module chain is cut at `MAX_MODULE_DEPTH` in
86/// `lang-rules/src/adapter/csharp.rs`)
87fn render_namespace(w: &mut Writer, node: &Namespace) {
88    // A `//` comment and not `///`: a namespace is not a documentable
89    // element, and an XML doc on one is CS1587 the moment anyone builds
90    // with `/doc`.
91    line_comment(w, &node.doc);
92    let mut head = String::from("namespace ");
93    head.push_str(&path_text(&node.name));
94    w.line(&head);
95    w.line("{");
96    w.indent();
97    for using in &node.usings {
98        render_using(w, using);
99    }
100    if !node.usings.is_empty() && !node.items.is_empty() {
101        w.blank_line();
102    }
103    render_items(w, &node.items);
104    w.dedent();
105    w.line("}");
106}
107
108fn render_using(w: &mut Writer, node: &Using) {
109    line_comment(w, &node.doc);
110    let mut line = String::new();
111    if node.global {
112        line.push_str("global ");
113    }
114    line.push_str("using ");
115    if node.static_ {
116        line.push_str("static ");
117    }
118    if let Some(alias) = &node.alias {
119        line.push_str(alias.as_str());
120        line.push_str(" = ");
121    }
122    line.push_str(&type_text(&node.target));
123    line.push(';');
124    w.line(&line);
125}
126
127fn render_type_decl(w: &mut Writer, node: &TypeDecl) {
128    match node {
129        TypeDecl::Class(inner) => render_class(w, inner),
130        TypeDecl::Struct(inner) => render_struct(w, inner),
131        TypeDecl::Interface(inner) => render_interface(w, inner),
132        TypeDecl::Enum(inner) => render_enum(w, inner),
133        TypeDecl::Record(inner) => render_record(w, inner),
134        TypeDecl::Delegate(inner) => render_delegate(w, inner),
135    }
136}
137
138fn render_class(w: &mut Writer, node: &Class) {
139    doc(w, &node.doc);
140    attributes(w, &node.attributes);
141    let mut head = modifiers_text(&node.modifiers);
142    head.push_str("class ");
143    head.push_str(node.name.as_str());
144    head.push_str(&generics_text(&node.generics));
145    head.push_str(&bases_text(&node.bases));
146    w.line(&head);
147    constraints(w, &node.constraints);
148    render_body(w, &node.members);
149}
150
151fn render_struct(w: &mut Writer, node: &Struct) {
152    doc(w, &node.doc);
153    attributes(w, &node.attributes);
154    let mut head = modifiers_text(&node.modifiers);
155    head.push_str("struct ");
156    head.push_str(node.name.as_str());
157    head.push_str(&generics_text(&node.generics));
158    head.push_str(&bases_text(&node.bases));
159    w.line(&head);
160    constraints(w, &node.constraints);
161    render_body(w, &node.members);
162}
163
164fn render_interface(w: &mut Writer, node: &Interface) {
165    doc(w, &node.doc);
166    attributes(w, &node.attributes);
167    let mut head = modifiers_text(&node.modifiers);
168    head.push_str("interface ");
169    head.push_str(node.name.as_str());
170    head.push_str(&generics_text(&node.generics));
171    head.push_str(&bases_text(&node.bases));
172    w.line(&head);
173    constraints(w, &node.constraints);
174    render_body(w, &node.members);
175}
176
177fn render_record(w: &mut Writer, node: &Record) {
178    doc(w, &node.doc);
179    attributes(w, &node.attributes);
180    let mut head = modifiers_text(&node.modifiers);
181    head.push_str(node.kind.as_str());
182    head.push(' ');
183    head.push_str(node.name.as_str());
184    head.push_str(&generics_text(&node.generics));
185    if let Some(params) = &node.parameters {
186        head.push('(');
187        head.push_str(&params_text(params));
188        head.push(')');
189    }
190    head.push_str(&bases_text(&node.bases));
191    // A record with no members is one line: `record class P(int X);`.
192    if node.members.is_empty() && node.constraints.is_empty() {
193        head.push(';');
194        w.line(&head);
195        return;
196    }
197    w.line(&head);
198    constraints(w, &node.constraints);
199    render_body(w, &node.members);
200}
201
202fn render_delegate(w: &mut Writer, node: &Delegate) {
203    doc(w, &node.doc);
204    attributes(w, &node.attributes);
205    let mut line = modifiers_text(&node.modifiers);
206    line.push_str("delegate ");
207    line.push_str(&type_text(&node.returns));
208    line.push(' ');
209    line.push_str(node.name.as_str());
210    line.push_str(&generics_text(&node.generics));
211    line.push('(');
212    line.push_str(&params_text(&node.params));
213    line.push(')');
214    if node.constraints.is_empty() {
215        line.push(';');
216        w.line(&line);
217        return;
218    }
219    w.line(&line);
220    w.indent();
221    for constraint in &node.constraints {
222        let mut text = constraint_text(constraint);
223        text.push(';');
224        w.line(&text);
225    }
226    w.dedent();
227}
228
229fn render_enum(w: &mut Writer, node: &Enum) {
230    doc(w, &node.doc);
231    attributes(w, &node.attributes);
232    let mut head = modifiers_text(&node.modifiers);
233    head.push_str("enum ");
234    head.push_str(node.name.as_str());
235    if let Some(underlying) = node.underlying {
236        head.push_str(" : ");
237        head.push_str(underlying.as_str());
238    }
239    w.line(&head);
240    w.line("{");
241    w.indent();
242    for member in &node.members {
243        render_enum_member(w, member);
244    }
245    w.dedent();
246    w.line("}");
247}
248
249/// Every member ends in a comma, the last one included.
250///
251/// C# allows the trailing comma, and writing it means adding a member is
252/// a one-line diff rather than a two-line one.
253fn render_enum_member(w: &mut Writer, node: &EnumMember) {
254    doc(w, &node.doc);
255    attributes(w, &node.attributes);
256    let mut line = String::from(node.name.as_str());
257    if let Some(value) = &node.value {
258        line.push_str(" = ");
259        line.push_str(&expr_text(value));
260    }
261    line.push(',');
262    w.line(&line);
263}
264
265fn render_body(w: &mut Writer, members: &[Member]) {
266    w.line("{");
267    w.indent();
268    render_members(w, members);
269    w.dedent();
270    w.line("}");
271}
272
273/// Fields and comments run together; anything with a body gets a blank
274/// line before it.
275fn render_members(w: &mut Writer, members: &[Member]) {
276    let mut previous_had_body = false;
277    for (index, member) in members.iter().enumerate() {
278        let has_body = !matches!(member, Member::Field(_) | Member::Comment(_));
279        if index > 0 && (has_body || previous_had_body) {
280            w.blank_line();
281        }
282        render_member(w, member);
283        previous_had_body = has_body;
284    }
285}
286
287fn render_member(w: &mut Writer, node: &Member) {
288    match node {
289        Member::Field(inner) => render_field(w, inner),
290        Member::Property(inner) => render_property(w, inner),
291        Member::Method(inner) => render_method(w, inner),
292        Member::Constructor(inner) => render_constructor(w, inner),
293        Member::Nested(decl) => render_type_decl(w, decl),
294        Member::Comment(inner) => line_comment(w, inner),
295        Member::Verbatim(text) => verbatim_lines(w, text),
296    }
297}
298
299fn render_field(w: &mut Writer, node: &Field) {
300    doc(w, &node.doc);
301    attributes(w, &node.attributes);
302    let mut line = modifiers_text(&node.modifiers);
303    line.push_str(&type_text(&node.type_));
304    line.push(' ');
305    line.push_str(node.name.as_str());
306    if let Some(value) = &node.value {
307        line.push_str(" = ");
308        line.push_str(&expr_text(value));
309    }
310    line.push(';');
311    w.line(&line);
312}
313
314fn render_property(w: &mut Writer, node: &Property) {
315    doc(w, &node.doc);
316    attributes(w, &node.attributes);
317    let mut head = modifiers_text(&node.modifiers);
318    head.push_str(&type_text(&node.type_));
319    head.push(' ');
320    head.push_str(node.name.as_str());
321
322    if let Some(expression) = &node.expression {
323        head.push_str(" => ");
324        head.push_str(&expr_text(expression));
325        head.push(';');
326        w.line(&head);
327        return;
328    }
329
330    // Auto-accessors go on one line; a body puts each on its own.
331    let auto = node
332        .accessors
333        .iter()
334        .all(|accessor| accessor.body.is_none() && accessor.expression.is_none());
335    if auto {
336        head.push_str(" { ");
337        for accessor in &node.accessors {
338            head.push_str(&modifiers_text(&accessor.modifiers));
339            head.push_str(accessor.kind.as_str());
340            head.push_str("; ");
341        }
342        head.push('}');
343        if let Some(initialiser) = &node.initialiser {
344            head.push_str(" = ");
345            head.push_str(&expr_text(initialiser));
346            head.push(';');
347        }
348        w.line(&head);
349        return;
350    }
351
352    w.line(&head);
353    w.line("{");
354    w.indent();
355    for accessor in &node.accessors {
356        render_accessor(w, accessor);
357    }
358    w.dedent();
359    w.line("}");
360}
361
362fn render_accessor(w: &mut Writer, node: &Accessor) {
363    let mut head = modifiers_text(&node.modifiers);
364    head.push_str(node.kind.as_str());
365    if let Some(expression) = &node.expression {
366        head.push_str(" => ");
367        head.push_str(&expr_text(expression));
368        head.push(';');
369        w.line(&head);
370        return;
371    }
372    match &node.body {
373        None => {
374            head.push(';');
375            w.line(&head);
376        }
377        Some(block) => {
378            w.line(&head);
379            w.line("{");
380            w.indent();
381            render_block(w, block);
382            w.dedent();
383            w.line("}");
384        }
385    }
386}
387
388fn render_method(w: &mut Writer, node: &Method) {
389    doc(w, &node.doc);
390    attributes(w, &node.attributes);
391    let mut head = modifiers_text(&node.modifiers);
392    head.push_str(&type_text(&node.returns));
393    head.push(' ');
394    head.push_str(node.name.as_str());
395    head.push_str(&generics_text(&node.generics));
396    head.push('(');
397    head.push_str(&params_text(&node.params));
398    head.push(')');
399    if let Some(expression) = &node.expression {
400        head.push_str(" => ");
401        head.push_str(&expr_text(expression));
402        head.push(';');
403        w.line(&head);
404        constraints(w, &node.constraints);
405        return;
406    }
407    match &node.body {
408        None => {
409            head.push(';');
410            w.line(&head);
411            constraints(w, &node.constraints);
412        }
413        Some(block) => {
414            w.line(&head);
415            constraints(w, &node.constraints);
416            w.line("{");
417            w.indent();
418            render_block(w, block);
419            w.dedent();
420            w.line("}");
421        }
422    }
423}
424
425fn render_constructor(w: &mut Writer, node: &Constructor) {
426    doc(w, &node.doc);
427    attributes(w, &node.attributes);
428    let mut head = modifiers_text(&node.modifiers);
429    head.push_str(node.name.as_str());
430    head.push('(');
431    head.push_str(&params_text(&node.params));
432    head.push(')');
433    if let Some(initialiser) = &node.initialiser {
434        head.push_str(" : ");
435        head.push_str(if initialiser.this { "this(" } else { "base(" });
436        let parts: Vec<String> = initialiser.args.iter().map(expr_text).collect();
437        head.push_str(&parts.join(", "));
438        head.push(')');
439    }
440    w.line(&head);
441    w.line("{");
442    w.indent();
443    render_block(w, &node.body);
444    w.dedent();
445    w.line("}");
446}
447
448/// zerodds-lint: recursion-depth = the generated body's block nesting.
449/// The codec walk emits a routine per type rather than inlining one
450/// (`lang-rules/src/emit/encode.rs`), so nothing here follows the type
451/// graph. What does grow with the IDL is the dispatch chain: a union's
452/// cases, an enumeration's enumerators and a `@mutable` aggregate's
453/// members each become one `If` per arm, in the previous arm's `else`.
454/// The model bounds none of the three counts, and C# nests the arms in
455/// its own output too, so they cannot be walked iteratively the way
456/// `c.rs` walks them. No number is claimed: this signature returns no
457/// error, so nothing here could report one.
458fn render_block(w: &mut Writer, block: &Block) {
459    for statement in &block.statements {
460        render_statement(w, statement);
461    }
462}
463
464/// zerodds-lint: recursion-depth = the generated body's block nesting.
465/// The codec walk emits a routine per type rather than inlining one
466/// (`lang-rules/src/emit/encode.rs`), so nothing here follows the type
467/// graph. What does grow with the IDL is the dispatch chain: a union's
468/// cases, an enumeration's enumerators and a `@mutable` aggregate's
469/// members each become one `If` per arm, in the previous arm's `else`.
470/// The model bounds none of the three counts, and C# nests the arms in
471/// its own output too, so they cannot be walked iteratively the way
472/// `c.rs` walks them. No number is claimed: this signature returns no
473/// error, so nothing here could report one.
474#[allow(clippy::too_many_lines)]
475fn render_statement(w: &mut Writer, node: &Statement) {
476    match node {
477        Statement::Declare { type_, name, value } => {
478            let mut line = declare_text(type_, name, value.as_ref());
479            line.push(';');
480            w.line(&line);
481        }
482        Statement::Expr(value) => {
483            let mut line = expr_text(value);
484            line.push(';');
485            w.line(&line);
486        }
487        Statement::Return(value) => {
488            let mut line = String::from("return");
489            if let Some(value) = value {
490                line.push(' ');
491                line.push_str(&expr_text(value));
492            }
493            line.push(';');
494            w.line(&line);
495        }
496        Statement::If {
497            condition,
498            then,
499            otherwise,
500        } => {
501            let mut head = String::from("if (");
502            head.push_str(&expr_text(condition));
503            head.push(')');
504            w.line(&head);
505            w.line("{");
506            w.indent();
507            render_block(w, then);
508            w.dedent();
509            w.line("}");
510            if let Some(block) = otherwise {
511                w.line("else");
512                w.line("{");
513                w.indent();
514                render_block(w, block);
515                w.dedent();
516                w.line("}");
517            }
518        }
519        Statement::ForEach {
520            type_,
521            binding,
522            range,
523            body,
524        } => {
525            let mut head = String::from("foreach (");
526            head.push_str(&type_text(type_));
527            head.push(' ');
528            head.push_str(binding.as_str());
529            head.push_str(" in ");
530            head.push_str(&expr_text(range));
531            head.push(')');
532            w.line(&head);
533            block_braces(w, body);
534        }
535        Statement::For {
536            init,
537            condition,
538            step,
539            body,
540        } => {
541            let mut head = String::from("for (");
542            head.push_str(&inline_statement_text(init));
543            head.push_str("; ");
544            head.push_str(&expr_text(condition));
545            head.push_str("; ");
546            head.push_str(&expr_text(step));
547            head.push(')');
548            w.line(&head);
549            block_braces(w, body);
550        }
551        Statement::While { condition, body } => {
552            let mut head = String::from("while (");
553            head.push_str(&expr_text(condition));
554            head.push(')');
555            w.line(&head);
556            block_braces(w, body);
557        }
558        Statement::Switch { value, sections } => {
559            let mut head = String::from("switch (");
560            head.push_str(&expr_text(value));
561            head.push(')');
562            w.line(&head);
563            w.line("{");
564            w.indent();
565            for section in sections {
566                render_section(w, section);
567            }
568            w.dedent();
569            w.line("}");
570        }
571        Statement::Throw(value) => {
572            let mut line = String::from("throw ");
573            line.push_str(&expr_text(value));
574            line.push(';');
575            w.line(&line);
576        }
577        Statement::Using { resource, body } => {
578            let mut head = String::from("using (");
579            head.push_str(&inline_statement_text(resource));
580            head.push(')');
581            w.line(&head);
582            block_braces(w, body);
583        }
584        Statement::Block(block) => block_braces(w, block),
585        Statement::Break => w.line("break;"),
586        Statement::Comment(node) => line_comment(w, node),
587    }
588}
589
590fn block_braces(w: &mut Writer, body: &Block) {
591    w.line("{");
592    w.indent();
593    render_block(w, body);
594    w.dedent();
595    w.line("}");
596}
597
598/// A statement written inside a `for` or `using` head, with no `;`.
599fn inline_statement_text(node: &Statement) -> String {
600    match node {
601        Statement::Declare { type_, name, value } => declare_text(type_, name, value.as_ref()),
602        Statement::Expr(value) => expr_text(value),
603        // Nothing else belongs in a head. Written as empty rather than
604        // guessed at: an unbalanced brace would be worse than a clause
605        // the reader can see is missing.
606        _ => String::new(),
607    }
608}
609
610fn declare_text(type_: &Type, name: &Ident, value: Option<&Expr>) -> String {
611    let mut out = type_text(type_);
612    out.push(' ');
613    out.push_str(name.as_str());
614    if let Some(value) = value {
615        out.push_str(" = ");
616        out.push_str(&expr_text(value));
617    }
618    out
619}
620
621fn render_section(w: &mut Writer, section: &SwitchSection) {
622    if section.labels.is_empty() {
623        w.line("default:");
624    } else {
625        for label in &section.labels {
626            let mut line = String::from("case ");
627            line.push_str(&expr_text(label));
628            line.push(':');
629            w.line(&line);
630        }
631    }
632    w.indent();
633    render_block(w, &section.body);
634    if section.breaks {
635        w.line("break;");
636    }
637    w.dedent();
638}
639
640fn modifiers_text(modifiers: &[Modifier]) -> String {
641    let mut out = String::new();
642    for modifier in modifiers {
643        out.push_str(modifier.as_str());
644        out.push(' ');
645    }
646    out
647}
648
649fn attributes(w: &mut Writer, attributes: &[Attribute]) {
650    for attribute in attributes {
651        w.line(&attribute_text(attribute));
652    }
653}
654
655fn attribute_text(attribute: &Attribute) -> String {
656    let mut out = String::from("[");
657    if let Some(target) = &attribute.target {
658        out.push_str(target.as_str());
659        out.push_str(": ");
660    }
661    out.push_str(&path_text(&attribute.path));
662    if !attribute.args.is_empty() {
663        out.push('(');
664        let parts: Vec<String> = attribute
665            .args
666            .iter()
667            .map(|arg| match arg {
668                AttributeArg::Positional(value) => expr_text(value),
669                AttributeArg::Named { name, value } => {
670                    let mut text = name.as_str().to_string();
671                    text.push_str(" = ");
672                    text.push_str(&expr_text(value));
673                    text
674                }
675            })
676            .collect();
677        out.push_str(&parts.join(", "));
678        out.push(')');
679    }
680    out.push(']');
681    out
682}
683
684fn generics_text(params: &[TypeParam]) -> String {
685    if params.is_empty() {
686        return String::new();
687    }
688    let mut out = String::from("<");
689    let parts: Vec<String> = params
690        .iter()
691        .map(|param| {
692            let mut text = String::new();
693            if let Some(variance) = param.variance {
694                text.push_str(variance.as_str());
695                text.push(' ');
696            }
697            text.push_str(param.name.as_str());
698            text
699        })
700        .collect();
701    out.push_str(&parts.join(", "));
702    out.push('>');
703    out
704}
705
706fn bases_text(bases: &[Type]) -> String {
707    if bases.is_empty() {
708        return String::new();
709    }
710    let parts: Vec<String> = bases.iter().map(type_text).collect();
711    let mut out = String::from(" : ");
712    out.push_str(&parts.join(", "));
713    out
714}
715
716fn constraints(w: &mut Writer, clauses: &[TypeConstraint]) {
717    if clauses.is_empty() {
718        return;
719    }
720    w.indent();
721    for clause in clauses {
722        w.line(&constraint_text(clause));
723    }
724    w.dedent();
725}
726
727fn constraint_text(clause: &TypeConstraint) -> String {
728    let mut out = String::from("where ");
729    out.push_str(clause.param.as_str());
730    out.push_str(" : ");
731    out.push_str(&clause.bounds.join(", "));
732    out
733}
734
735fn params_text(params: &[Param]) -> String {
736    let parts: Vec<String> = params
737        .iter()
738        .map(|param| {
739            let mut text = String::new();
740            for attribute in &param.attributes {
741                text.push_str(&attribute_text(attribute));
742                text.push(' ');
743            }
744            if let Some(modifier) = param.modifier {
745                text.push_str(modifier.as_str());
746                text.push(' ');
747            }
748            text.push_str(&type_text(&param.type_));
749            text.push(' ');
750            text.push_str(param.name.as_str());
751            if let Some(default) = &param.default {
752                text.push_str(" = ");
753                text.push_str(&expr_text(default));
754            }
755            text
756        })
757        .collect();
758    parts.join(", ")
759}
760
761/// A qualified name, with `global::` where the path is rooted.
762///
763/// The prefix is the whole reason a rooted path exists in the tree: it
764/// resolves at the global namespace and nothing declared in a scope can
765/// capture it.
766fn path_text(path: &Path) -> String {
767    let parts: Vec<&str> = path.segments().iter().map(Ident::as_str).collect();
768    let joined = parts.join(".");
769    if path.is_rooted() {
770        let mut out = String::from("global::");
771        out.push_str(&joined);
772        return out;
773    }
774    joined
775}
776
777/// zerodds-lint: recursion-depth 64 (one level per collection, alias or
778/// generic argument of the spelled type; `MAX_TYPE_DEPTH` in
779/// `lang-rules/src/adapter/csharp.rs` refuses a type that nests deeper)
780fn type_text(node: &Type) -> String {
781    match node {
782        Type::Predefined(predefined) => predefined.as_str().to_string(),
783        Type::Named { path, args } => {
784            let mut out = path_text(path);
785            if !args.is_empty() {
786                out.push('<');
787                let parts: Vec<String> = args.iter().map(type_text).collect();
788                out.push_str(&parts.join(", "));
789                out.push('>');
790            }
791            out
792        }
793        Type::Array { element, rank } => {
794            let mut out = type_text(element);
795            out.push('[');
796            // `T[,]` for rank two: one comma fewer than the rank.
797            for _ in 1..*rank {
798                out.push(',');
799            }
800            out.push(']');
801            out
802        }
803        Type::Nullable(inner) => {
804            let mut out = type_text(inner);
805            out.push('?');
806            out
807        }
808        Type::Pointer(inner) => {
809            let mut out = type_text(inner);
810            out.push('*');
811            out
812        }
813        Type::Tuple(elements) => {
814            let parts: Vec<String> = elements
815                .iter()
816                .map(|element| {
817                    let mut text = type_text(&element.type_);
818                    if let Some(name) = &element.name {
819                        text.push(' ');
820                        text.push_str(name.as_str());
821                    }
822                    text
823                })
824                .collect();
825            let mut out = String::from("(");
826            out.push_str(&parts.join(", "));
827            out.push(')');
828            out
829        }
830        Type::Var => "var".to_string(),
831    }
832}
833
834/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
835/// calls a routine per type rather than nesting one, so the one operand
836/// tree that grows with the IDL is an array's default value: one composite
837/// literal per dimension, built in `lang-rules/src/default_csharp.rs`, and
838/// nothing bounds the dimension count. No number is claimed: this
839/// signature returns no error.
840#[allow(clippy::too_many_lines)]
841fn expr_text(node: &Expr) -> String {
842    match node {
843        Expr::Literal(literal) => literal_text(literal),
844        Expr::Path(path) => path_text(path),
845        Expr::Invoke {
846            receiver,
847            name,
848            args,
849        } => {
850            let mut out = String::new();
851            if let Some(receiver) = receiver {
852                out.push_str(&expr_text(receiver));
853                out.push('.');
854            }
855            out.push_str(name.as_str());
856            out.push('(');
857            let parts: Vec<String> = args.iter().map(expr_text).collect();
858            out.push_str(&parts.join(", "));
859            out.push(')');
860            out
861        }
862        Expr::Member { value, name } => {
863            let mut out = expr_text(value);
864            out.push('.');
865            out.push_str(name.as_str());
866            out
867        }
868        Expr::Index { value, index } => {
869            let mut out = expr_text(value);
870            out.push('[');
871            out.push_str(&expr_text(index));
872            out.push(']');
873            out
874        }
875        Expr::New {
876            type_,
877            args,
878            initialiser,
879        } => {
880            let mut out = String::from("new ");
881            out.push_str(&type_text(type_));
882            out.push('(');
883            let parts: Vec<String> = args.iter().map(expr_text).collect();
884            out.push_str(&parts.join(", "));
885            out.push(')');
886            if !initialiser.is_empty() {
887                out.push_str(" { ");
888                let parts: Vec<String> = initialiser
889                    .iter()
890                    .map(|(name, value)| {
891                        let mut text = name.as_str().to_string();
892                        text.push_str(" = ");
893                        text.push_str(&expr_text(value));
894                        text
895                    })
896                    .collect();
897                out.push_str(&parts.join(", "));
898                out.push_str(" }");
899            }
900            out
901        }
902        Expr::NewArray {
903            element,
904            length,
905            elements,
906            nested,
907        } => {
908            let mut out = String::from("new ");
909            out.push_str(&type_text(element));
910            match length {
911                Some(length) => {
912                    out.push('[');
913                    out.push_str(&expr_text(length));
914                    out.push(']');
915                    // The inner ranks of a jagged array, left empty:
916                    // `new int[3][]` and never `new int[][3]`.
917                    for _ in 0..*nested {
918                        out.push_str("[]");
919                    }
920                }
921                None => {
922                    out.push_str("[]");
923                    for _ in 0..*nested {
924                        out.push_str("[]");
925                    }
926                    out.push_str(" { ");
927                    let parts: Vec<String> = elements.iter().map(expr_text).collect();
928                    out.push_str(&parts.join(", "));
929                    out.push_str(" }");
930                }
931            }
932            out
933        }
934        Expr::Binary { op, lhs, rhs } => {
935            let mut out = expr_text(lhs);
936            out.push(' ');
937            out.push_str(op);
938            out.push(' ');
939            out.push_str(&expr_text(rhs));
940            out
941        }
942        Expr::Unary { op, value } => {
943            let mut out = (*op).to_string();
944            out.push_str(&expr_text(value));
945            out
946        }
947        Expr::Cast { type_, value } => {
948            let mut out = String::from("(");
949            out.push_str(&type_text(type_));
950            out.push(')');
951            out.push_str(&expr_text(value));
952            out
953        }
954        Expr::Is { value, type_ } => {
955            let mut out = expr_text(value);
956            out.push_str(" is ");
957            out.push_str(&type_text(type_));
958            out
959        }
960        Expr::Conditional {
961            condition,
962            then,
963            otherwise,
964        } => {
965            let mut out = expr_text(condition);
966            out.push_str(" ? ");
967            out.push_str(&expr_text(then));
968            out.push_str(" : ");
969            out.push_str(&expr_text(otherwise));
970            out
971        }
972        Expr::Assign { target, value } => {
973            let mut out = expr_text(target);
974            out.push_str(" = ");
975            out.push_str(&expr_text(value));
976            out
977        }
978        Expr::Paren(inner) => {
979            let mut out = String::from("(");
980            out.push_str(&expr_text(inner));
981            out.push(')');
982            out
983        }
984        Expr::This => "this".to_string(),
985        Expr::Default(type_) => match type_ {
986            None => "default".to_string(),
987            Some(type_) => {
988                let mut out = String::from("default(");
989                out.push_str(&type_text(type_));
990                out.push(')');
991                out
992            }
993        },
994        Expr::TypeOf(type_) => {
995            let mut out = String::from("typeof(");
996            out.push_str(&type_text(type_));
997            out.push(')');
998            out
999        }
1000        Expr::NameOf(value) => {
1001            let mut out = String::from("nameof(");
1002            out.push_str(&expr_text(value));
1003            out.push(')');
1004            out
1005        }
1006        Expr::Lambda { params, body } => {
1007            let mut out = String::from("(");
1008            let parts: Vec<&str> = params.iter().map(Ident::as_str).collect();
1009            out.push_str(&parts.join(", "));
1010            out.push_str(") => ");
1011            out.push_str(&expr_text(body));
1012            out
1013        }
1014    }
1015}
1016
1017fn literal_text(literal: &Literal) -> String {
1018    match literal {
1019        Literal::Int(text) | Literal::Float(text) => text.clone(),
1020        Literal::Bool(value) => {
1021            if *value {
1022                "true".to_string()
1023            } else {
1024                "false".to_string()
1025            }
1026        }
1027        Literal::Char(ch) => {
1028            let mut out = String::from("'");
1029            out.push_str(&escape_char(*ch));
1030            out.push('\'');
1031            out
1032        }
1033        Literal::Str(text) => {
1034            let mut out = String::from("\"");
1035            out.push_str(&escape_string(text));
1036            out.push('"');
1037            out
1038        }
1039        Literal::Bytes(bytes) => {
1040            let mut out = String::from("new byte[] { ");
1041            let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("0x{b:02X}")).collect();
1042            out.push_str(&parts.join(", "));
1043            out.push_str(" }");
1044            out
1045        }
1046        Literal::Null => "null".to_string(),
1047    }
1048}
1049
1050/// A `///` XML documentation block.
1051///
1052/// The text is escaped for XML. A comment carrying `sequence<long>` or
1053/// `a && b` is prose to the reader and markup to the compiler, and the
1054/// moment anyone builds with `/doc` an unescaped one is a warning about
1055/// a file this crate generated.
1056fn doc(w: &mut Writer, doc: &Doc) {
1057    if doc.is_empty() {
1058        return;
1059    }
1060    w.line("/// <summary>");
1061    let escaped: Vec<String> = doc.as_lines().iter().map(|line| escape_xml(line)).collect();
1062    comment_lines(w, "/// ", &escaped);
1063    w.line("/// </summary>");
1064}
1065
1066/// A `//` comment block, for text that is not a member's documentation.
1067///
1068/// A file header and a note beside a declaration are not XML doc: they
1069/// document a decision rather than an API, and `///` on something that
1070/// is not a declaration is a warning (CS1587) rather than a comment.
1071fn line_comment(w: &mut Writer, doc: &Doc) {
1072    if doc.is_empty() {
1073        return;
1074    }
1075    comment_lines(w, "// ", doc.as_lines());
1076}
1077
1078/// The three characters XML gives a meaning to.
1079fn escape_xml(text: &str) -> String {
1080    let mut out = String::new();
1081    for ch in text.chars() {
1082        match ch {
1083            '&' => out.push_str("&amp;"),
1084            '<' => out.push_str("&lt;"),
1085            '>' => out.push_str("&gt;"),
1086            other => out.push(other),
1087        }
1088    }
1089    out
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
1095    use super::*;
1096    use zerodds_lang_cst::common::Visibility;
1097    use zerodds_lang_cst::csharp::{Predefined, RecordKind};
1098
1099    fn class(name: &str, members: Vec<Member>) -> TypeDecl {
1100        TypeDecl::Class(Class {
1101            doc: Doc::none(),
1102            attributes: Vec::new(),
1103            modifiers: alloc::vec![Modifier::Public, Modifier::Sealed],
1104            name: Ident::new(name),
1105            generics: Vec::new(),
1106            bases: Vec::new(),
1107            constraints: Vec::new(),
1108            members,
1109        })
1110    }
1111
1112    fn field(type_: Type, name: &str) -> Member {
1113        Member::Field(Field {
1114            doc: Doc::none(),
1115            attributes: Vec::new(),
1116            modifiers: alloc::vec![Modifier::Public],
1117            type_,
1118            name: Ident::new(name),
1119            value: None,
1120        })
1121    }
1122
1123    #[test]
1124    fn a_class_opens_its_brace_on_a_line_of_its_own() {
1125        // The .NET convention, and the one difference in shape from the
1126        // Java renderer beside it.
1127        let text = type_decl(&class("Pose", alloc::vec![]));
1128        assert_eq!(text, "public sealed class Pose\n{\n}\n");
1129    }
1130
1131    #[test]
1132    fn a_rooted_path_renders_with_global() {
1133        let type_ = Type::named(Path::rooted(&["a", "b", "Point"]));
1134        assert_eq!(type_text(&type_), "global::a.b.Point");
1135        let relative = Type::named(Path::new(&["a", "b", "Point"]));
1136        assert_eq!(type_text(&relative), "a.b.Point");
1137    }
1138
1139    #[test]
1140    fn an_array_renders_one_pair_of_brackets_per_level() {
1141        let jagged = Type::array(Type::array(Type::Predefined(Predefined::Int)));
1142        assert_eq!(type_text(&jagged), "int[][]");
1143        let rectangular = Type::Array {
1144            element: alloc::boxed::Box::new(Type::Predefined(Predefined::Int)),
1145            rank: 2,
1146        };
1147        assert_eq!(type_text(&rectangular), "int[,]");
1148    }
1149
1150    #[test]
1151    fn a_nullable_value_type_renders_with_a_question_mark() {
1152        let type_ = Type::Nullable(alloc::boxed::Box::new(Type::Predefined(Predefined::Int)));
1153        assert_eq!(type_text(&type_), "int?");
1154    }
1155
1156    #[test]
1157    fn a_namespace_nests_and_its_members_are_indented() {
1158        let text = file(&File {
1159            doc: Doc::none(),
1160            usings: Vec::new(),
1161            items: alloc::vec![Item::Namespace(Namespace {
1162                doc: Doc::none(),
1163                name: Path::new(&["a", "b"]),
1164                usings: Vec::new(),
1165                items: alloc::vec![Item::Type(class(
1166                    "Point",
1167                    alloc::vec![field(Type::Predefined(Predefined::Double), "X")]
1168                ))],
1169            })],
1170        });
1171        assert!(text.contains("namespace a.b\n{\n"), "{text}");
1172        assert!(text.contains("    public sealed class Point\n"), "{text}");
1173        assert!(text.contains("        public double X;\n"), "{text}");
1174    }
1175
1176    #[test]
1177    fn an_enum_writes_its_underlying_type_and_a_trailing_comma() {
1178        let text = type_decl(&TypeDecl::Enum(Enum {
1179            doc: Doc::none(),
1180            attributes: Vec::new(),
1181            modifiers: alloc::vec![Modifier::Public],
1182            name: Ident::new("Colour"),
1183            underlying: Some(Predefined::Int),
1184            members: alloc::vec![EnumMember {
1185                doc: Doc::none(),
1186                attributes: Vec::new(),
1187                name: Ident::new("RED"),
1188                value: Some(Expr::Literal(Literal::int(0))),
1189            }],
1190        }));
1191        assert!(text.contains("public enum Colour : int\n"), "{text}");
1192        assert!(text.contains("    RED = 0,\n"), "{text}");
1193    }
1194
1195    #[test]
1196    fn an_attribute_renders_above_what_it_annotates() {
1197        let member = Member::Field(Field {
1198            doc: Doc::none(),
1199            attributes: alloc::vec![Attribute {
1200                target: None,
1201                path: Path::rooted(&["ZeroDds", "Idl", "IdlName"]),
1202                args: alloc::vec![AttributeArg::Positional(Expr::Literal(Literal::string(
1203                    "venue"
1204                )))],
1205            }],
1206            modifiers: alloc::vec![Modifier::Public],
1207            type_: Type::Predefined(Predefined::Int),
1208            name: Ident::new("Venue"),
1209            value: None,
1210        });
1211        let text = type_decl(&class("P", alloc::vec![member]));
1212        assert!(
1213            text.contains("[global::ZeroDds.Idl.IdlName(\"venue\")]"),
1214            "{text}"
1215        );
1216        assert!(text.contains("public int Venue;"), "{text}");
1217    }
1218
1219    #[test]
1220    fn an_xml_doc_escapes_the_characters_xml_reads() {
1221        // `sequence<long>` in a comment is prose to a reader and an
1222        // element to the compiler.
1223        let mut w = Writer::spaces4();
1224        doc(&mut w, &Doc::line("a sequence<long> & an array"));
1225        let text = w.finish();
1226        assert!(
1227            text.contains("/// a sequence&lt;long&gt; &amp; an array"),
1228            "{text}"
1229        );
1230        assert!(text.starts_with("/// <summary>"), "{text}");
1231    }
1232
1233    #[test]
1234    fn a_file_header_is_a_plain_comment_and_not_xml_doc() {
1235        // `///` above something that is not a declaration is CS1587.
1236        let text = file(&File {
1237            doc: Doc::line("Generated. Do not edit."),
1238            usings: Vec::new(),
1239            items: Vec::new(),
1240        });
1241        assert!(text.starts_with("// Generated. Do not edit.\n"), "{text}");
1242        assert!(!text.contains("///"), "{text}");
1243    }
1244
1245    #[test]
1246    fn a_using_alias_renders_its_target_type() {
1247        let mut w = Writer::spaces4();
1248        render_using(
1249            &mut w,
1250            &Using {
1251                doc: Doc::none(),
1252                alias: Some(Ident::new("Row")),
1253                static_: false,
1254                global: false,
1255                target: Type::array(Type::Predefined(Predefined::Int)),
1256            },
1257        );
1258        assert_eq!(w.finish(), "using Row = int[];\n");
1259    }
1260
1261    #[test]
1262    fn an_auto_property_stays_on_one_line() {
1263        let member = Member::Property(Property {
1264            doc: Doc::none(),
1265            attributes: Vec::new(),
1266            modifiers: alloc::vec![Modifier::Public],
1267            type_: Type::Predefined(Predefined::Int),
1268            name: Ident::new("Count"),
1269            accessors: alloc::vec![
1270                Accessor {
1271                    kind: zerodds_lang_cst::csharp::AccessorKind::Get,
1272                    modifiers: Vec::new(),
1273                    body: None,
1274                    expression: None,
1275                },
1276                Accessor {
1277                    kind: zerodds_lang_cst::csharp::AccessorKind::Set,
1278                    modifiers: Vec::new(),
1279                    body: None,
1280                    expression: None,
1281                },
1282            ],
1283            expression: None,
1284            initialiser: None,
1285        });
1286        let text = type_decl(&class("P", alloc::vec![member]));
1287        assert!(text.contains("public int Count { get; set; }"), "{text}");
1288    }
1289
1290    #[test]
1291    fn a_record_with_no_body_ends_in_a_semicolon() {
1292        let text = type_decl(&TypeDecl::Record(Record {
1293            doc: Doc::none(),
1294            attributes: Vec::new(),
1295            modifiers: alloc::vec![Modifier::Public],
1296            kind: RecordKind::Class,
1297            name: Ident::new("P"),
1298            generics: Vec::new(),
1299            parameters: Some(alloc::vec![Param {
1300                attributes: Vec::new(),
1301                modifier: None,
1302                type_: Type::Predefined(Predefined::Int),
1303                name: Ident::new("X"),
1304                default: None,
1305            }]),
1306            bases: Vec::new(),
1307            constraints: Vec::new(),
1308            members: Vec::new(),
1309        }));
1310        assert_eq!(text, "public record class P(int X);\n");
1311    }
1312
1313    #[test]
1314    fn the_renderer_decides_nothing_the_tree_did_not_say() {
1315        // Visibility is a common-tree concept the C# tree does not use:
1316        // C# spells its access as modifiers, and a renderer that mapped
1317        // one onto the other would be deciding.
1318        assert_eq!(Visibility::ALL.len(), 3);
1319        let text = type_decl(&class("P", alloc::vec![]));
1320        assert!(!text.contains("internal"), "{text}");
1321    }
1322}