1use 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#[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#[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#[must_use]
57pub fn type_(node: &Type) -> String {
58 type_text(node)
59}
60
61#[must_use]
63pub fn expr(node: &Expr) -> String {
64 expr_text(node)
65}
66
67fn 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
84fn render_namespace(w: &mut Writer, node: &Namespace) {
88 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(¶ms_text(params));
188 head.push(')');
189 }
190 head.push_str(&bases_text(&node.bases));
191 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(¶ms_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
249fn 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
273fn 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 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(¶ms_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(¶ms_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
448fn render_block(w: &mut Writer, block: &Block) {
459 for statement in &block.statements {
460 render_statement(w, statement);
461 }
462}
463
464#[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
598fn 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 _ => 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 §ion.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, §ion.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 ¶m.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(¶m.type_));
749 text.push(' ');
750 text.push_str(param.name.as_str());
751 if let Some(default) = ¶m.default {
752 text.push_str(" = ");
753 text.push_str(&expr_text(default));
754 }
755 text
756 })
757 .collect();
758 parts.join(", ")
759}
760
761fn 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
777fn 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 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#[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 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
1050fn 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
1066fn line_comment(w: &mut Writer, doc: &Doc) {
1072 if doc.is_empty() {
1073 return;
1074 }
1075 comment_lines(w, "// ", doc.as_lines());
1076}
1077
1078fn escape_xml(text: &str) -> String {
1080 let mut out = String::new();
1081 for ch in text.chars() {
1082 match ch {
1083 '&' => out.push_str("&"),
1084 '<' => out.push_str("<"),
1085 '>' => out.push_str(">"),
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 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 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<long> & 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 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 assert_eq!(Visibility::ALL.len(), 3);
1319 let text = type_decl(&class("P", alloc::vec![]));
1320 assert!(!text.contains("internal"), "{text}");
1321 }
1322}