irongit
jv/src/codegen.rs
723 lines27 KBRust
1use std::collections::BTreeSet;
2
3use crate::types::InferredType;
4
5pub struct CodeGenerator {
6 pub structs: Vec<GeneratedStruct>,
7}
8
9#[derive(Debug, Clone)]
10pub struct GeneratedStruct {
11 pub name: String,
12 pub fields: Vec<GeneratedField>,
13}
14
15/// Structured representation of a resolved type, replacing string-based manipulation.
16#[derive(Debug, Clone, PartialEq)]
17pub enum ResolvedType {
18 /// A known struct name (e.g., "Passenger")
19 Struct(String),
20 /// Vec<inner>
21 Array(Box<ResolvedType>),
22 /// Option<inner>
23 Optional(Box<ResolvedType>),
24 /// Primitive type — use lang.type_name() to render
25 Inferred(InferredType),
26}
27
28impl ResolvedType {
29 /// Render to language-specific code (e.g., `Vec<Option<Passenger>>` for Rust, `[Passenger?]` for Swift)
30 pub fn to_code(&self, lang: &dyn crate::lang::LanguageGenerator) -> String {
31 match self {
32 ResolvedType::Struct(name) => name.clone(),
33 ResolvedType::Array(inner) => lang.wrap_array(&inner.to_code(lang)),
34 ResolvedType::Optional(inner) => lang.wrap_optional(&inner.to_code(lang)),
35 ResolvedType::Inferred(t) => lang.type_name(t),
36 }
37 }
38
39 /// Prefix struct names that aren't in shared_names and aren't the root
40 pub fn prefix_structs(
41 &self,
42 prefix: &str,
43 shared_names: &std::collections::BTreeSet<String>,
44 root_name: &str,
45 ) -> ResolvedType {
46 match self {
47 ResolvedType::Struct(name) => {
48 if !shared_names.contains(name)
49 && name != root_name
50 && !name.starts_with(prefix)
51 {
52 ResolvedType::Struct(format!("{}{}", prefix, name))
53 } else {
54 self.clone()
55 }
56 }
57 ResolvedType::Array(inner) => {
58 ResolvedType::Array(Box::new(inner.prefix_structs(prefix, shared_names, root_name)))
59 }
60 ResolvedType::Optional(inner) => {
61 ResolvedType::Optional(Box::new(inner.prefix_structs(prefix, shared_names, root_name)))
62 }
63 ResolvedType::Inferred(_) => self.clone(),
64 }
65 }
66
67 /// Extract all struct names referenced in this type
68 pub fn struct_names(&self) -> Vec<&str> {
69 match self {
70 ResolvedType::Struct(name) => vec![name.as_str()],
71 ResolvedType::Array(inner) | ResolvedType::Optional(inner) => inner.struct_names(),
72 ResolvedType::Inferred(_) => vec![],
73 }
74 }
75
76 /// Wrap in Optional if not already optional
77 pub fn make_optional(self) -> ResolvedType {
78 match self {
79 ResolvedType::Optional(_) => self,
80 other => ResolvedType::Optional(Box::new(other)),
81 }
82 }
83
84 pub fn is_optional(&self) -> bool {
85 matches!(self, ResolvedType::Optional(_))
86 }
87}
88
89#[derive(Debug, Clone)]
90pub struct GeneratedField {
91 pub json_name: String,
92 pub inferred_type: InferredType,
93 /// Resolved struct type — None means use inferred_type via lang.type_name()
94 pub resolved_type: Option<ResolvedType>,
95 pub needs_rename: bool,
96}
97
98impl CodeGenerator {
99 pub fn from_value(value: &serde_json::Value) -> Self {
100 Self::from_value_named(value, "Root")
101 }
102
103 pub fn from_value_named(value: &serde_json::Value, root_name: &str) -> Self {
104 let mut structs = Vec::new();
105 let mut seen_names = BTreeSet::new();
106 Self::collect_structs(value, root_name, &mut structs, &mut seen_names);
107 Self { structs }
108 }
109
110 pub fn from_schema(shared: &[crate::schema::SharedStruct]) -> Self {
111 let mut structs = Vec::new();
112
113 for s in shared {
114 let fields = s
115 .fields
116 .iter()
117 .map(|(key, typ)| {
118 let snake = to_snake_case(key);
119 let needs_rename = snake != *key;
120 let resolved_type = resolve_type_to_struct(typ, shared);
121 GeneratedField {
122 json_name: key.clone(),
123 inferred_type: typ.clone(),
124 resolved_type,
125 needs_rename,
126 }
127 })
128 .collect();
129
130 structs.push(GeneratedStruct {
131 name: s.name.clone(),
132 fields,
133 });
134 }
135
136 Self { structs }
137 }
138
139 fn collect_structs(
140 value: &serde_json::Value,
141 name: &str,
142 structs: &mut Vec<GeneratedStruct>,
143 seen: &mut BTreeSet<String>,
144 ) {
145 match value {
146 serde_json::Value::Object(map) => {
147 let mut fields = Vec::new();
148 for (key, val) in map {
149 let typ = crate::types::infer_type(val);
150 let snake = to_snake_case(key);
151 let needs_rename = snake != *key;
152
153 let resolved_type = match val {
154 serde_json::Value::Object(_) => {
155 let child_name = to_pascal_case(key);
156 Self::collect_structs(val, &child_name, structs, seen);
157 Some(ResolvedType::Struct(child_name))
158 }
159 serde_json::Value::Array(arr) => {
160 if let Some(first) = arr.first() {
161 if first.is_object() {
162 let child_name = to_pascal_case(&singularize(key));
163 Self::collect_structs(first, &child_name, structs, seen);
164 Some(ResolvedType::Array(Box::new(ResolvedType::Struct(child_name))))
165 } else {
166 None
167 }
168 } else {
169 None
170 }
171 }
172 _ => None,
173 };
174
175 fields.push(GeneratedField {
176 json_name: key.clone(),
177 inferred_type: typ,
178 resolved_type,
179 needs_rename,
180 });
181 }
182
183 // Check if an existing struct with this name has identical fields
184 if seen.contains(name) {
185 let field_sig: Vec<(&str, &InferredType, Option<&ResolvedType>)> = fields
186 .iter()
187 .map(|f| (f.json_name.as_str(), &f.inferred_type, f.resolved_type.as_ref()))
188 .collect();
189 let already_exists = structs.iter().any(|s| {
190 s.name == name && s.fields.len() == fields.len() && {
191 let existing_sig: Vec<(&str, &InferredType, Option<&ResolvedType>)> = s.fields
192 .iter()
193 .map(|f| (f.json_name.as_str(), &f.inferred_type, f.resolved_type.as_ref()))
194 .collect();
195 existing_sig == field_sig
196 }
197 });
198 if already_exists {
199 return;
200 }
201 }
202
203 let unique_name = make_unique_name(name, seen);
204 seen.insert(unique_name.clone());
205
206 structs.push(GeneratedStruct {
207 name: unique_name,
208 fields,
209 });
210 }
211 serde_json::Value::Array(arr) => {
212 if let Some(first) = arr.first() {
213 Self::collect_structs(first, name, structs, seen);
214 }
215 }
216 _ => {}
217 }
218 }
219
220 pub fn generate_code(&self, lang: &dyn crate::lang::LanguageGenerator) -> String {
221 // Generate struct bodies first so we know what types are used
222 let mut body = String::new();
223 for (i, s) in self.structs.iter().rev().enumerate() {
224 if i > 0 {
225 body.push('\n');
226 }
227 body.push_str(&lang.struct_open(&s.name));
228
229 let mut field_pairs: Vec<(String, String)> = Vec::new();
230 for field in &s.fields {
231 let code_name = lang.field_name(&field.json_name);
232 let type_str = match &field.resolved_type {
233 Some(rt) => rt.to_code(lang),
234 None => lang.type_name(&field.inferred_type),
235 };
236 body.push_str(&lang.field_line(&code_name, &type_str, &field.json_name));
237 field_pairs.push((code_name, field.json_name.clone()));
238 }
239
240 body.push_str(&lang.struct_close(&field_pairs));
241 }
242
243 // Build output with imports based on what the body actually uses
244 let mut output = String::new();
245 output.push_str(&lang.file_header());
246 output.push_str(&lang.imports_header(&body, false));
247 if !output.is_empty() && !output.ends_with('\n') {
248 output.push('\n');
249 }
250 output.push_str(&body);
251
252 output
253 }
254}
255
256/// A generated project file (name + code content)
257pub struct GeneratedProjectFile {
258 pub name: String,
259 pub code: String,
260 /// Root type for each source file that contributed to this group.
261 /// Vec of (source_filename, root_rust_type) — used for deserialization testing.
262 pub root_types: Vec<(String, String)>,
263}
264
265/// Generate all Rust files for a project: shared.rs, per-group files, mod.rs.
266/// This mirrors the code view's `rebuild_file_mode` pipeline exactly.
267pub fn generate_project(
268 parsed_files: &[(String, serde_json::Value)],
269 schema: &crate::schema::SchemaOverview,
270 lang: &dyn crate::lang::LanguageGenerator,
271) -> Vec<GeneratedProjectFile> {
272 use std::collections::{BTreeMap, BTreeSet};
273
274 let shared_names: BTreeSet<String> = schema.structs.iter().map(|s| s.name.clone()).collect();
275 let unique_names: BTreeSet<String> = schema.unique_structs.iter().map(|s| s.name.clone()).collect();
276 let all_structs = schema.all_structs();
277
278 let mut result = Vec::new();
279
280 // shared.rs
281 if !schema.structs.is_empty() {
282 let code = CodeGenerator::from_schema(&schema.structs).generate_code(lang);
283 result.push(GeneratedProjectFile {
284 name: lang.file_name("shared"),
285 code,
286 root_types: Vec::new(),
287 });
288 }
289
290 // Group files by depluralized first word
291 let mut groups: BTreeMap<String, Vec<(&str, &serde_json::Value)>> = BTreeMap::new();
292 for (filename, value) in parsed_files {
293 let word = first_normal_word(filename)
294 .map(|w| to_pascal_case(&singularize(&w)))
295 .unwrap_or_else(|| "other".to_string());
296 let key = singularize(&word.to_ascii_lowercase());
297 groups.entry(key).or_default().push((filename.as_str(), value));
298 }
299
300 let all_schema_names: BTreeSet<String> = shared_names.iter().chain(unique_names.iter()).cloned().collect();
301
302 for (group_key, files) in &groups {
303 // Collect all struct definitions, merging fields from multiple files
304 let mut struct_order: Vec<String> = Vec::new();
305 let mut struct_defs: BTreeMap<String, Vec<GeneratedField>> = BTreeMap::new();
306 let mut root_types: Vec<(String, String)> = Vec::new();
307 let mut type_aliases: Vec<String> = Vec::new();
308 let mut seen_aliases: BTreeSet<String> = BTreeSet::new();
309
310 for (filename, value) in files {
311 let prefix = first_normal_word(filename)
312 .map(|w| to_pascal_case(&singularize(&w)))
313 .unwrap_or_default();
314 let is_root_array = value.is_array();
315
316 let (root_name, array_item_name) = if is_root_array {
317 let singular = singularize(&prefix);
318 let item_name = if singular.is_empty() {
319 "Item".to_string()
320 } else {
321 let mut s = String::new();
322 s.push(singular.chars().next().unwrap().to_ascii_uppercase());
323 s.extend(singular.chars().skip(1));
324 s
325 };
326 (item_name.clone(), Some(item_name))
327 } else {
328 let name = if prefix.is_empty() { "Root".to_string() } else { format!("{}Root", prefix) };
329 (name, None)
330 };
331
332 let deser_type = if is_root_array {
333 format!("Vec<{}>", root_name)
334 } else {
335 root_name.clone()
336 };
337 root_types.push((filename.to_string(), deser_type));
338
339 let mut gen = CodeGenerator::from_value_named(value, &root_name);
340
341 // Schema-aware type resolution
342 resolve_codegen_against_schema(&mut gen, &all_structs, &shared_names);
343
344 // Collect structs, merging duplicates
345 for s in gen.structs.iter().rev() {
346 if shared_names.contains(&s.name) {
347 continue;
348 }
349
350 let prefixed = format!("{}{}", prefix, s.name);
351 let struct_name = if unique_names.contains(&prefixed) {
352 prefixed
353 } else if unique_names.contains(&s.name) {
354 s.name.clone()
355 } else if s.name != root_name && !prefix.is_empty() && !s.name.starts_with(&prefix) {
356 prefixed
357 } else {
358 s.name.clone()
359 };
360
361 if let Some(existing) = struct_defs.get_mut(&struct_name) {
362 // Merge: make fields Optional if missing or Null in this instance
363 merge_generated_fields(existing, &s.fields);
364 } else {
365 struct_order.push(struct_name.clone());
366 struct_defs.insert(struct_name, s.fields.clone());
367 }
368 }
369
370 if let Some(ref item_name) = array_item_name {
371 let alias_name = format!("{}Root", prefix);
372 if !seen_aliases.contains(&alias_name) {
373 seen_aliases.insert(alias_name.clone());
374 let aliased = if shared_names.contains(item_name) {
375 item_name.clone()
376 } else if !prefix.is_empty() && !item_name.starts_with(&prefix) {
377 format!("{}{}", prefix, item_name)
378 } else {
379 item_name.clone()
380 };
381 type_aliases.push(format!("pub type {} = Vec<{}>;\n", alias_name, aliased));
382 }
383 }
384 }
385
386 // Emit code from merged struct definitions
387 let group_prefix = to_pascal_case(&singularize(group_key));
388 let mut struct_blocks: Vec<String> = type_aliases;
389 for struct_name in &struct_order {
390 let fields = &struct_defs[struct_name];
391 let prefix = group_prefix.clone();
392
393 let mut code = String::new();
394 code.push_str(&lang.struct_open(struct_name));
395 let mut field_pairs: Vec<(String, String)> = Vec::new();
396 for field in fields {
397 let code_name = lang.field_name(&field.json_name);
398 let type_str = match &field.resolved_type {
399 Some(rt) => {
400 let prefixed = if !prefix.is_empty() {
401 rt.prefix_structs(&prefix, &all_schema_names, struct_name)
402 } else {
403 rt.clone()
404 };
405 prefixed.to_code(lang)
406 }
407 None => lang.type_name(&field.inferred_type),
408 };
409 code.push_str(&lang.field_line(&code_name, &type_str, &field.json_name));
410 field_pairs.push((code_name, field.json_name.clone()));
411 }
412 code.push_str(&lang.struct_close(&field_pairs));
413 struct_blocks.push(code);
414 }
415
416 let mut body = String::new();
417 for block in &struct_blocks {
418 body.push_str(block);
419 body.push('\n');
420 }
421
422 let mut code = String::new();
423 let header = lang.file_header();
424 if !header.is_empty() {
425 code.push_str(&header);
426 code.push('\n');
427 }
428 code.push_str(&lang.imports_header(&body, !shared_names.is_empty()));
429 code.push('\n');
430 code.push_str(&body);
431
432 result.push(GeneratedProjectFile {
433 name: lang.file_name(group_key),
434 code: code.trim_end().to_string() + "\n",
435 root_types,
436 });
437 }
438
439 // mod.rs
440 let mod_names: Vec<&str> = result.iter().map(|f| {
441 f.name.strip_suffix(".rs").unwrap_or(&f.name)
442 }).collect();
443 if let Some(mod_code) = lang.mod_file(&mod_names) {
444 result.push(GeneratedProjectFile {
445 name: "mod.rs".to_string(),
446 code: mod_code,
447 root_types: Vec::new(),
448 });
449 }
450
451 result
452}
453
454/// Resolve types in a CodeGenerator against schema structs, and generate
455/// missing struct definitions from schema when referenced but not present.
456pub fn resolve_codegen_against_schema(
457 gen: &mut CodeGenerator,
458 all_structs: &[crate::schema::SharedStruct],
459 shared_names: &std::collections::BTreeSet<String>,
460) {
461 for s in &mut gen.structs {
462 let schema_match = crate::types::resolve_struct_name(
463 &s.fields.iter().map(|f| (f.json_name.clone(), f.inferred_type.clone())).collect(),
464 all_structs,
465 );
466 let schema_fields = schema_match.and_then(|name| {
467 all_structs.iter().find(|ss| ss.name == name)
468 });
469
470 for field in &mut s.fields {
471 if field.resolved_type.is_none() {
472 field.resolved_type = resolve_type_to_struct(&field.inferred_type, all_structs);
473 if field.resolved_type.is_none() {
474 if let Some(ss) = schema_fields {
475 if let Some(schema_type) = ss.fields.get(&field.json_name) {
476 field.resolved_type = resolve_type_to_struct(schema_type, all_structs);
477 if field.resolved_type.is_some() {
478 field.inferred_type = schema_type.clone();
479 }
480 }
481 }
482 }
483 }
484 }
485 }
486
487 // Generate missing structs from schema
488 let existing_names: std::collections::BTreeSet<String> =
489 gen.structs.iter().map(|s| s.name.clone()).collect();
490 let mut needed: Vec<String> = Vec::new();
491 for s in &gen.structs {
492 for field in &s.fields {
493 if let Some(rt) = &field.resolved_type {
494 for name in rt.struct_names() {
495 if !existing_names.contains(name) && !shared_names.contains(name) {
496 needed.push(name.to_string());
497 }
498 }
499 }
500 }
501 }
502 let mut added: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
503 while let Some(name) = needed.pop() {
504 if added.contains(&name) || existing_names.contains(&name) || shared_names.contains(&name) {
505 continue;
506 }
507 added.insert(name.clone());
508 if let Some(ss) = all_structs.iter().find(|ss| ss.name == name) {
509 let fields: Vec<GeneratedField> = ss.fields.iter().map(|(key, typ)| {
510 let resolved = resolve_type_to_struct(typ, all_structs);
511 if let Some(rt) = &resolved {
512 for dep in rt.struct_names() {
513 needed.push(dep.to_string());
514 }
515 }
516 GeneratedField {
517 json_name: key.clone(),
518 inferred_type: typ.clone(),
519 resolved_type: resolved,
520 needs_rename: false,
521 }
522 }).collect();
523 gen.structs.push(GeneratedStruct {
524 name: name.clone(),
525 fields,
526 });
527 }
528 }
529}
530
531/// Merge a new set of fields into an existing field list.
532/// Fields missing from the new set, or whose type is Null, become Option<T>.
533fn merge_generated_fields(existing: &mut Vec<GeneratedField>, new_fields: &[GeneratedField]) {
534 use std::collections::BTreeMap;
535
536 let new_map: BTreeMap<&str, &GeneratedField> = new_fields
537 .iter()
538 .map(|f| (f.json_name.as_str(), f))
539 .collect();
540
541 for field in existing.iter_mut() {
542 let should_optionalize = match new_map.get(field.json_name.as_str()) {
543 None => true,
544 Some(new_field) => new_field.inferred_type == InferredType::Null,
545 };
546
547 if should_optionalize
548 && !matches!(field.inferred_type, InferredType::Option(_) | InferredType::Null)
549 {
550 field.inferred_type = InferredType::Option(Box::new(field.inferred_type.clone()));
551 field.resolved_type = field.resolved_type.take().map(|rt| rt.make_optional());
552 }
553 }
554
555 // Add fields that exist in new but not in existing (as Optional)
556 let existing_names: BTreeSet<String> = existing.iter().map(|f| f.json_name.clone()).collect();
557 for new_field in new_fields {
558 if !existing_names.contains(&new_field.json_name) {
559 let mut field = new_field.clone();
560 if !matches!(field.inferred_type, InferredType::Option(_) | InferredType::Null) {
561 field.inferred_type = InferredType::Option(Box::new(field.inferred_type.clone()));
562 field.resolved_type = field.resolved_type.take().map(|rt| rt.make_optional());
563 }
564 existing.push(field);
565 }
566 }
567}
568
569pub fn to_snake_case(s: &str) -> String {
570 let mut result = String::new();
571 for (i, ch) in s.chars().enumerate() {
572 if ch.is_ascii_uppercase() {
573 if i > 0 {
574 let prev = s.chars().nth(i - 1).unwrap_or('_');
575 if prev != '_' && !prev.is_ascii_uppercase() {
576 result.push('_');
577 }
578 }
579 result.push(ch.to_ascii_lowercase());
580 } else if ch == '-' || ch == ' ' {
581 result.push('_');
582 } else {
583 result.push(ch);
584 }
585 }
586 // Ensure it's a valid Rust identifier
587 if result.starts_with(|c: char| c.is_ascii_digit()) {
588 result.insert(0, '_');
589 }
590 sanitize_keyword(&result)
591}
592
593pub fn to_pascal_case(s: &str) -> String {
594 let mut result = String::new();
595 let mut capitalize_next = true;
596 for ch in s.chars() {
597 if ch == '_' || ch == '-' || ch == ' ' {
598 capitalize_next = true;
599 } else if capitalize_next {
600 result.push(ch.to_ascii_uppercase());
601 capitalize_next = false;
602 } else {
603 result.push(ch);
604 }
605 }
606 result
607}
608
609pub fn singularize(s: &str) -> String {
610 let lower = s.to_ascii_lowercase();
611
612 // Words that end in 's' but aren't plural
613 const NOT_PLURAL: &[&str] = &[
614 "status", "address", "bus", "canvas", "atlas", "alias", "basis",
615 "radius", "focus", "census", "corpus", "consensus", "osis",
616 "sis", "nexus", "plus", "minus", "gas", "class", "pass",
617 "process", "access", "success", "progress", "express",
618 ];
619 for &word in NOT_PLURAL {
620 if lower == word || lower.ends_with(word) {
621 return s.to_string();
622 }
623 }
624
625 if lower.ends_with("ies") && lower.len() > 4 {
626 // categories -> category, companies -> company
627 format!("{}y", &s[..s.len() - 3])
628 } else if lower.ends_with("ses") || lower.ends_with("xes") || lower.ends_with("zes") {
629 // responses -> response, indexes -> index, buzzes -> buzz
630 // but not "ses" alone
631 if lower.len() > 4 {
632 s[..s.len() - 2].to_string()
633 } else {
634 s.to_string()
635 }
636 } else if lower.ends_with("ves") {
637 // leaves -> leaf (but this is rare in JSON, just strip the s)
638 s[..s.len() - 1].to_string()
639 } else if lower.ends_with('s')
640 && !lower.ends_with("ss")
641 && !lower.ends_with("us")
642 && !lower.ends_with("is")
643 {
644 // trips -> trip, users -> user
645 s[..s.len() - 1].to_string()
646 } else {
647 s.to_string()
648 }
649}
650
651/// Extract the first "normal" word from a filename (skip dates, numbers, version strings)
652/// e.g. "trip_29_feb2026_rv0.json" -> "trip", "users.json" -> "users"
653/// Returns lowercase, not PascalCased — caller decides casing.
654pub fn first_normal_word(filename: &str) -> Option<String> {
655 let stem = std::path::Path::new(filename)
656 .file_stem()
657 .and_then(|s| s.to_str())
658 .unwrap_or(filename);
659
660 for part in stem.split(|c: char| c == '_' || c == '-' || c == ' ' || c == '.') {
661 if part.is_empty() {
662 continue;
663 }
664 if part.chars().all(|c| c.is_ascii_digit()) {
665 continue;
666 }
667 if part.len() <= 4 && part.chars().any(|c| c.is_ascii_digit()) {
668 continue;
669 }
670 return Some(part.to_ascii_lowercase());
671 }
672 None
673}
674
675/// Resolve an InferredType to a ResolvedType if it contains an Object matching a known struct.
676/// Recursively handles Vec<Object>, Option<Object>, Option<Vec<Object>>, etc.
677pub fn resolve_type_to_struct(
678 typ: &InferredType,
679 shared: &[crate::schema::SharedStruct],
680) -> Option<ResolvedType> {
681 match typ {
682 InferredType::Object(fields) => {
683 crate::types::resolve_struct_name(fields, shared).map(ResolvedType::Struct)
684 }
685 InferredType::Array(inner) => {
686 resolve_type_to_struct(inner, shared)
687 .map(|rt| ResolvedType::Array(Box::new(rt)))
688 }
689 InferredType::Option(inner) => {
690 resolve_type_to_struct(inner, shared)
691 .map(|rt| ResolvedType::Optional(Box::new(rt)))
692 }
693 _ => None,
694 }
695}
696
697fn make_unique_name(name: &str, seen: &BTreeSet<String>) -> String {
698 if !seen.contains(name) {
699 return name.to_string();
700 }
701 let mut i = 2;
702 loop {
703 let candidate = format!("{}{}", name, i);
704 if !seen.contains(&candidate) {
705 return candidate;
706 }
707 i += 1;
708 }
709}
710
711fn sanitize_keyword(s: &str) -> String {
712 match s {
713 "type" | "struct" | "enum" | "fn" | "let" | "mut" | "ref" | "self" | "super" | "mod"
714 | "use" | "pub" | "crate" | "impl" | "trait" | "for" | "loop" | "while" | "if"
715 | "else" | "match" | "return" | "break" | "continue" | "move" | "async" | "await"
716 | "dyn" | "static" | "const" | "where" | "unsafe" | "extern" | "as" | "in"
717 | "override" | "abstract" | "become" | "box" | "do" | "final" | "macro" | "priv"
718 | "try" | "typeof" | "unsized" | "virtual" | "yield" => {
719 format!("r#{}", s)
720 }
721 _ => s.to_string(),
722 }
723}