1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum TargetLanguage {
34 Python,
36 TypeScript,
38 Rust,
40 Ruby,
42 Php,
44}
45
46impl TargetLanguage {
47 #[must_use]
49 pub const fn reserved_keywords(self) -> &'static [&'static str] {
50 match self {
51 Self::Python => PYTHON_KEYWORDS,
52 Self::TypeScript => TYPESCRIPT_KEYWORDS,
53 Self::Rust => RUST_KEYWORDS,
54 Self::Ruby => RUBY_KEYWORDS,
55 Self::Php => PHP_KEYWORDS,
56 }
57 }
58
59 #[must_use]
61 pub const fn soft_keywords(self) -> &'static [&'static str] {
62 match self {
63 Self::Python => PYTHON_SOFT_KEYWORDS,
64 Self::TypeScript => TYPESCRIPT_SOFT_KEYWORDS,
65 Self::Rust => RUST_SOFT_KEYWORDS,
66 Self::Ruby => RUBY_SOFT_KEYWORDS,
67 Self::Php => PHP_SOFT_KEYWORDS,
68 }
69 }
70
71 #[must_use]
75 pub const fn keyword_prefix(self) -> &'static str {
76 match self {
77 Self::Rust => "r#",
78 _ => "_",
79 }
80 }
81}
82
83const PYTHON_KEYWORDS: &[&str] = &[
86 "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del",
87 "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal",
88 "not", "or", "pass", "raise", "return", "try", "while", "with", "yield",
89];
90
91const PYTHON_SOFT_KEYWORDS: &[&str] = &["match", "case", "type"];
93
94const TYPESCRIPT_KEYWORDS: &[&str] = &[
97 "abstract",
98 "any",
99 "as",
100 "async",
101 "await",
102 "boolean",
103 "break",
104 "case",
105 "catch",
106 "class",
107 "const",
108 "continue",
109 "debugger",
110 "declare",
111 "default",
112 "delete",
113 "do",
114 "else",
115 "enum",
116 "export",
117 "extends",
118 "false",
119 "finally",
120 "for",
121 "from",
122 "function",
123 "get",
124 "global",
125 "if",
126 "implements",
127 "import",
128 "in",
129 "instanceof",
130 "interface",
131 "is",
132 "keyof",
133 "let",
134 "module",
135 "namespace",
136 "never",
137 "new",
138 "null",
139 "number",
140 "of",
141 "package",
142 "private",
143 "protected",
144 "public",
145 "readonly",
146 "require",
147 "return",
148 "set",
149 "static",
150 "string",
151 "super",
152 "switch",
153 "symbol",
154 "this",
155 "throw",
156 "true",
157 "try",
158 "type",
159 "typeof",
160 "unique",
161 "var",
162 "void",
163 "while",
164 "with",
165 "yield",
166];
167
168const TYPESCRIPT_SOFT_KEYWORDS: &[&str] = &["as", "require", "get", "set", "accessor"];
170
171const RUST_KEYWORDS: &[&str] = &[
174 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false", "fn",
175 "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self",
176 "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while",
177];
178
179const RUST_SOFT_KEYWORDS: &[&str] = &[
181 "abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof", "unsized", "virtual", "yield",
182];
183
184const RUBY_KEYWORDS: &[&str] = &[
187 "BEGIN",
188 "END",
189 "__ENCODING__",
190 "__FILE__",
191 "__LINE__",
192 "alias",
193 "and",
194 "begin",
195 "break",
196 "case",
197 "class",
198 "def",
199 "defined?",
200 "do",
201 "else",
202 "elsif",
203 "end",
204 "ensure",
205 "false",
206 "for",
207 "if",
208 "in",
209 "module",
210 "next",
211 "nil",
212 "not",
213 "or",
214 "redo",
215 "rescue",
216 "retry",
217 "return",
218 "self",
219 "super",
220 "then",
221 "true",
222 "undef",
223 "unless",
224 "until",
225 "when",
226 "while",
227 "yield",
228];
229
230const RUBY_SOFT_KEYWORDS: &[&str] = &[];
232
233const PHP_KEYWORDS: &[&str] = &[
236 "abstract",
237 "and",
238 "array",
239 "as",
240 "break",
241 "callable",
242 "case",
243 "catch",
244 "class",
245 "clone",
246 "const",
247 "continue",
248 "declare",
249 "default",
250 "die",
251 "do",
252 "echo",
253 "else",
254 "elseif",
255 "empty",
256 "enddeclare",
257 "endfor",
258 "endforeach",
259 "endif",
260 "endswitch",
261 "endwhile",
262 "eval",
263 "exit",
264 "extends",
265 "false",
266 "final",
267 "finally",
268 "fn",
269 "for",
270 "foreach",
271 "from",
272 "function",
273 "global",
274 "goto",
275 "if",
276 "implements",
277 "include",
278 "include_once",
279 "instanceof",
280 "insteadof",
281 "interface",
282 "isset",
283 "list",
284 "match",
285 "namespace",
286 "new",
287 "never",
288 "null",
289 "or",
290 "print",
291 "private",
292 "protected",
293 "public",
294 "readonly",
295 "require",
296 "require_once",
297 "return",
298 "static",
299 "switch",
300 "throw",
301 "trait",
302 "true",
303 "try",
304 "unset",
305 "use",
306 "var",
307 "while",
308 "xor",
309 "yield",
310];
311
312const PHP_SOFT_KEYWORDS: &[&str] = &["mixed", "object", "parent", "self", "static", "string", "void"];
314
315#[must_use]
357pub fn sanitize_identifier(name: &str, language: TargetLanguage) -> String {
358 if name.is_empty() {
359 return "field".to_string();
360 }
361
362 let mut ident: String = name
363 .chars()
364 .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
365 .collect();
366
367 while ident.contains("__") {
368 ident = ident.replace("__", "_");
369 }
370
371 ident = ident.trim_matches('_').to_string();
372
373 if ident.is_empty() {
374 return "field".to_string();
375 }
376
377 if ident.chars().next().unwrap().is_ascii_digit() {
378 ident.insert(0, '_');
379 }
380
381 let lower_ident = ident.to_lowercase();
382
383 let is_reserved = language
384 .reserved_keywords()
385 .iter()
386 .any(|kw| kw.to_lowercase() == lower_ident)
387 || language
388 .soft_keywords()
389 .iter()
390 .any(|kw| kw.to_lowercase() == lower_ident);
391
392 if is_reserved {
393 let prefix = language.keyword_prefix();
394 format!("{prefix}{lower_ident}")
395 } else {
396 lower_ident
397 }
398}
399
400#[must_use]
424pub fn sanitize_identifier_snake_case(name: &str, language: TargetLanguage) -> String {
425 let mut result = String::new();
426 let mut prev_was_upper = false;
427
428 for (i, c) in name.chars().enumerate() {
429 if c.is_uppercase() && i > 0 && !prev_was_upper {
430 result.push('_');
431 result.push(c.to_lowercase().next().unwrap());
432 prev_was_upper = true;
433 } else if c.is_uppercase() {
434 result.push(c.to_lowercase().next().unwrap());
435 prev_was_upper = true;
436 } else {
437 result.push(c);
438 prev_was_upper = false;
439 }
440 }
441
442 sanitize_identifier(&result, language)
443}
444
445#[must_use]
469pub fn sanitize_identifier_camel_case(name: &str, language: TargetLanguage) -> String {
470 let sanitized = sanitize_identifier(name, language);
471 let parts: Vec<&str> = sanitized.split('_').collect();
472
473 if parts.is_empty() {
474 return "field".to_string();
475 }
476
477 let mut result = parts[0].to_string();
478 for part in &parts[1..] {
479 if !part.is_empty() {
480 let mut chars = part.chars();
481 if let Some(first) = chars.next() {
482 result.push_str(&first.to_uppercase().to_string());
483 result.push_str(chars.as_str());
484 }
485 }
486 }
487
488 result
489}
490
491#[must_use]
515pub fn sanitize_identifier_pascal_case(name: &str, language: TargetLanguage) -> String {
516 let sanitized = sanitize_identifier(name, language);
517 let parts: Vec<&str> = sanitized.split('_').collect();
518
519 parts
520 .iter()
521 .filter(|part| !part.is_empty())
522 .map(|part| {
523 let mut chars = part.chars();
524 match chars.next() {
525 None => String::new(),
526 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
527 }
528 })
529 .collect()
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn test_python_reserved_keywords() {
538 assert_eq!(sanitize_identifier("class", TargetLanguage::Python), "_class");
539 assert_eq!(sanitize_identifier("def", TargetLanguage::Python), "_def");
540 assert_eq!(sanitize_identifier("import", TargetLanguage::Python), "_import");
541 assert_eq!(sanitize_identifier("if", TargetLanguage::Python), "_if");
542 assert_eq!(sanitize_identifier("while", TargetLanguage::Python), "_while");
543 assert_eq!(sanitize_identifier("and", TargetLanguage::Python), "_and");
544 assert_eq!(sanitize_identifier("or", TargetLanguage::Python), "_or");
545 assert_eq!(sanitize_identifier("not", TargetLanguage::Python), "_not");
546 assert_eq!(sanitize_identifier("for", TargetLanguage::Python), "_for");
547 assert_eq!(sanitize_identifier("return", TargetLanguage::Python), "_return");
548 }
549
550 #[test]
551 fn test_python_soft_keywords() {
552 assert_eq!(sanitize_identifier("match", TargetLanguage::Python), "_match");
553 assert_eq!(sanitize_identifier("case", TargetLanguage::Python), "_case");
554 assert_eq!(sanitize_identifier("type", TargetLanguage::Python), "_type");
555 }
556
557 #[test]
558 fn test_python_non_keywords() {
559 assert_eq!(sanitize_identifier("hello", TargetLanguage::Python), "hello");
560 assert_eq!(sanitize_identifier("world", TargetLanguage::Python), "world");
561 assert_eq!(sanitize_identifier("my_var", TargetLanguage::Python), "my_var");
562 }
563
564 #[test]
565 fn test_typescript_reserved_keywords() {
566 assert_eq!(sanitize_identifier("const", TargetLanguage::TypeScript), "_const");
567 assert_eq!(sanitize_identifier("let", TargetLanguage::TypeScript), "_let");
568 assert_eq!(sanitize_identifier("var", TargetLanguage::TypeScript), "_var");
569 assert_eq!(sanitize_identifier("function", TargetLanguage::TypeScript), "_function");
570 assert_eq!(sanitize_identifier("class", TargetLanguage::TypeScript), "_class");
571 assert_eq!(
572 sanitize_identifier("interface", TargetLanguage::TypeScript),
573 "_interface"
574 );
575 assert_eq!(sanitize_identifier("type", TargetLanguage::TypeScript), "_type");
576 }
577
578 #[test]
579 fn test_typescript_non_keywords() {
580 assert_eq!(sanitize_identifier("name", TargetLanguage::TypeScript), "name");
581 assert_eq!(sanitize_identifier("userId", TargetLanguage::TypeScript), "userid");
582 assert_eq!(
583 sanitize_identifier_camel_case("user_id", TargetLanguage::TypeScript),
584 "userId"
585 );
586 }
587
588 #[test]
589 fn test_rust_reserved_keywords() {
590 assert_eq!(sanitize_identifier("fn", TargetLanguage::Rust), "r#fn");
591 assert_eq!(sanitize_identifier("let", TargetLanguage::Rust), "r#let");
592 assert_eq!(sanitize_identifier("mut", TargetLanguage::Rust), "r#mut");
593 assert_eq!(sanitize_identifier("struct", TargetLanguage::Rust), "r#struct");
594 assert_eq!(sanitize_identifier("enum", TargetLanguage::Rust), "r#enum");
595 assert_eq!(sanitize_identifier("impl", TargetLanguage::Rust), "r#impl");
596 assert_eq!(sanitize_identifier("trait", TargetLanguage::Rust), "r#trait");
597 assert_eq!(sanitize_identifier("async", TargetLanguage::Rust), "r#async");
598 assert_eq!(sanitize_identifier("await", TargetLanguage::Rust), "r#await");
599 }
600
601 #[test]
602 fn test_rust_non_keywords() {
603 assert_eq!(sanitize_identifier("main", TargetLanguage::Rust), "main");
604 assert_eq!(sanitize_identifier("my_function", TargetLanguage::Rust), "my_function");
605 }
606
607 #[test]
608 fn test_ruby_reserved_keywords() {
609 assert_eq!(sanitize_identifier("def", TargetLanguage::Ruby), "_def");
610 assert_eq!(sanitize_identifier("class", TargetLanguage::Ruby), "_class");
611 assert_eq!(sanitize_identifier("module", TargetLanguage::Ruby), "_module");
612 assert_eq!(sanitize_identifier("if", TargetLanguage::Ruby), "_if");
613 assert_eq!(sanitize_identifier("unless", TargetLanguage::Ruby), "_unless");
614 assert_eq!(sanitize_identifier("case", TargetLanguage::Ruby), "_case");
615 assert_eq!(sanitize_identifier("when", TargetLanguage::Ruby), "_when");
616 assert_eq!(sanitize_identifier("return", TargetLanguage::Ruby), "_return");
617 }
618
619 #[test]
620 fn test_ruby_non_keywords() {
621 assert_eq!(sanitize_identifier("hello", TargetLanguage::Ruby), "hello");
622 assert_eq!(sanitize_identifier("my_var", TargetLanguage::Ruby), "my_var");
623 }
624
625 #[test]
626 fn test_php_reserved_keywords() {
627 assert_eq!(sanitize_identifier("abstract", TargetLanguage::Php), "_abstract");
628 assert_eq!(sanitize_identifier("class", TargetLanguage::Php), "_class");
629 assert_eq!(sanitize_identifier("function", TargetLanguage::Php), "_function");
630 assert_eq!(sanitize_identifier("interface", TargetLanguage::Php), "_interface");
631 assert_eq!(sanitize_identifier("namespace", TargetLanguage::Php), "_namespace");
632 assert_eq!(sanitize_identifier("use", TargetLanguage::Php), "_use");
633 assert_eq!(sanitize_identifier("return", TargetLanguage::Php), "_return");
634 }
635
636 #[test]
637 fn test_php_non_keywords() {
638 assert_eq!(sanitize_identifier("hello", TargetLanguage::Php), "hello");
639 assert_eq!(sanitize_identifier("my_class", TargetLanguage::Php), "my_class");
640 }
641
642 #[test]
643 fn test_sanitize_invalid_characters() {
644 assert_eq!(
645 sanitize_identifier("hello-world", TargetLanguage::Python),
646 "hello_world"
647 );
648 assert_eq!(
649 sanitize_identifier("hello world", TargetLanguage::Python),
650 "hello_world"
651 );
652 assert_eq!(
653 sanitize_identifier("hello@world#test", TargetLanguage::Python),
654 "hello_world_test"
655 );
656 assert_eq!(
657 sanitize_identifier("hello.world", TargetLanguage::Python),
658 "hello_world"
659 );
660 }
661
662 #[test]
663 fn test_sanitize_leading_digit() {
664 assert_eq!(sanitize_identifier("42answer", TargetLanguage::Python), "_42answer");
665 assert_eq!(sanitize_identifier("123start", TargetLanguage::TypeScript), "_123start");
666 assert_eq!(sanitize_identifier("1st_place", TargetLanguage::Rust), "_1st_place");
667 }
668
669 #[test]
670 fn test_sanitize_multiple_underscores() {
671 assert_eq!(
672 sanitize_identifier("hello__world", TargetLanguage::Python),
673 "hello_world"
674 );
675 assert_eq!(sanitize_identifier("__double__", TargetLanguage::TypeScript), "double");
676 assert_eq!(sanitize_identifier("___triple___", TargetLanguage::Ruby), "triple");
677 }
678
679 #[test]
680 fn test_sanitize_empty_and_invalid() {
681 assert_eq!(sanitize_identifier("", TargetLanguage::Python), "field");
682 assert_eq!(sanitize_identifier("---", TargetLanguage::TypeScript), "field");
683 assert_eq!(sanitize_identifier("___", TargetLanguage::Rust), "field");
684 assert_eq!(sanitize_identifier("@#$", TargetLanguage::Ruby), "field");
685 }
686
687 #[test]
688 fn test_sanitize_case_insensitive_keywords() {
689 assert_eq!(sanitize_identifier("CLASS", TargetLanguage::Python), "_class");
690 assert_eq!(sanitize_identifier("CLASS", TargetLanguage::TypeScript), "_class");
691 assert_eq!(sanitize_identifier("FN", TargetLanguage::Rust), "r#fn");
692 assert_eq!(sanitize_identifier("DEF", TargetLanguage::Ruby), "_def");
693 }
694
695 #[test]
696 fn test_sanitize_camel_case() {
697 assert_eq!(
698 sanitize_identifier_camel_case("hello_world", TargetLanguage::TypeScript),
699 "helloWorld"
700 );
701 assert_eq!(
702 sanitize_identifier_camel_case("my_function_name", TargetLanguage::TypeScript),
703 "myFunctionName"
704 );
705 assert_eq!(
706 sanitize_identifier_camel_case("hello_world_test", TargetLanguage::TypeScript),
707 "helloWorldTest"
708 );
709 }
710
711 #[test]
712 fn test_sanitize_pascal_case() {
713 assert_eq!(
714 sanitize_identifier_pascal_case("hello_world", TargetLanguage::Rust),
715 "HelloWorld"
716 );
717 assert_eq!(
718 sanitize_identifier_pascal_case("my_class_name", TargetLanguage::TypeScript),
719 "MyClassName"
720 );
721 assert_eq!(
722 sanitize_identifier_pascal_case("simple", TargetLanguage::Rust),
723 "Simple"
724 );
725 }
726
727 #[test]
728 fn test_sanitize_snake_case() {
729 assert_eq!(
730 sanitize_identifier_snake_case("HelloWorld", TargetLanguage::Python),
731 "hello_world"
732 );
733 assert_eq!(
734 sanitize_identifier_snake_case("MyFunctionName", TargetLanguage::Python),
735 "my_function_name"
736 );
737 assert_eq!(sanitize_identifier_snake_case("hello", TargetLanguage::Python), "hello");
738 }
739
740 #[test]
741 fn test_combined_keyword_and_format() {
742 let result = sanitize_identifier_camel_case("hello_class", TargetLanguage::TypeScript);
743 assert_eq!(result, "helloClass");
744 assert!(!result.starts_with('_'));
745
746 let result = sanitize_identifier_snake_case("HelloClass", TargetLanguage::Python);
747 assert_eq!(result, "hello_class");
748
749 let result = sanitize_identifier_snake_case("Class", TargetLanguage::Python);
750 assert_eq!(result, "_class");
751 }
752}