Skip to main content

alef_core/
keywords.rs

1//! Reserved keyword lists and field-name escaping for all supported language backends.
2//!
3//! Each language backend may encounter Rust field names that are reserved keywords
4//! in the target language. This module provides a central registry of those keywords
5//! and a function to compute the safe name to use in the generated binding.
6//!
7//! # Escape strategy
8//!
9//! When a field name is reserved in the target language it is escaped by appending
10//! a trailing underscore (e.g. `class` → `class_`).  The original name is preserved
11//! in language-level attribute annotations so the user-visible API still exposes the
12//! original name (e.g. `#[pyo3(get, name = "class")]`, `#[serde(rename = "class")]`).
13
14/// Python reserved keywords and soft-keywords that cannot be used as identifiers.
15///
16/// Includes the `type` soft-keyword (Python 3.12+) and the built-in constants
17/// `None`, `True`, `False` which are also reserved in identifier position.
18pub const PYTHON_KEYWORDS: &[&str] = &[
19    "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del",
20    "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal",
21    "not", "or", "pass", "raise", "return", "try", "type", "while", "with", "yield",
22];
23
24/// Python `str` instance methods that an enum member name can shadow.
25///
26/// `StrEnum` inherits from `str`, so variant names that match `str` instance methods
27/// (e.g., `Title` → `title`) shadow the inherited method and trigger mypy [assignment]
28/// errors at the class body. This constant lists all such methods that must be escaped
29/// with a trailing underscore when used as enum member names (e.g., `title_`).
30pub const PYTHON_STR_METHODS: &[&str] = &[
31    "capitalize",
32    "casefold",
33    "center",
34    "count",
35    "encode",
36    "endswith",
37    "expandtabs",
38    "find",
39    "format",
40    "format_map",
41    "index",
42    "isalnum",
43    "isalpha",
44    "isascii",
45    "isdecimal",
46    "isdigit",
47    "isidentifier",
48    "islower",
49    "isnumeric",
50    "isprintable",
51    "isspace",
52    "istitle",
53    "isupper",
54    "join",
55    "ljust",
56    "lower",
57    "lstrip",
58    "maketrans",
59    "partition",
60    "removeprefix",
61    "removesuffix",
62    "replace",
63    "rfind",
64    "rindex",
65    "rjust",
66    "rpartition",
67    "rsplit",
68    "rstrip",
69    "split",
70    "splitlines",
71    "startswith",
72    "strip",
73    "swapcase",
74    "title",
75    "translate",
76    "upper",
77    "zfill",
78];
79
80/// Java reserved keywords (including all contextual/reserved identifiers).
81pub const JAVA_KEYWORDS: &[&str] = &[
82    "abstract",
83    "assert",
84    "boolean",
85    "break",
86    "byte",
87    "case",
88    "catch",
89    "char",
90    "class",
91    "const",
92    "continue",
93    "default",
94    "do",
95    "double",
96    "else",
97    "enum",
98    "extends",
99    "final",
100    "finally",
101    "float",
102    "for",
103    "goto",
104    "if",
105    "implements",
106    "import",
107    "instanceof",
108    "int",
109    "interface",
110    "long",
111    "native",
112    "new",
113    "package",
114    "private",
115    "protected",
116    "public",
117    "return",
118    "short",
119    "static",
120    "strictfp",
121    "super",
122    "switch",
123    "synchronized",
124    "this",
125    "throw",
126    "throws",
127    "transient",
128    "try",
129    "void",
130    "volatile",
131    "while",
132];
133
134/// C# reserved keywords.
135pub const CSHARP_KEYWORDS: &[&str] = &[
136    "abstract",
137    "as",
138    "base",
139    "bool",
140    "break",
141    "byte",
142    "case",
143    "catch",
144    "char",
145    "checked",
146    "class",
147    "const",
148    "continue",
149    "decimal",
150    "default",
151    "delegate",
152    "do",
153    "double",
154    "else",
155    "enum",
156    "event",
157    "explicit",
158    "extern",
159    "false",
160    "finally",
161    "fixed",
162    "float",
163    "for",
164    "foreach",
165    "goto",
166    "if",
167    "implicit",
168    "in",
169    "int",
170    "interface",
171    "internal",
172    "is",
173    "lock",
174    "long",
175    "namespace",
176    "new",
177    "null",
178    "object",
179    "operator",
180    "out",
181    "override",
182    "params",
183    "private",
184    "protected",
185    "public",
186    "readonly",
187    "ref",
188    "return",
189    "sbyte",
190    "sealed",
191    "short",
192    "sizeof",
193    "stackalloc",
194    "static",
195    "string",
196    "struct",
197    "switch",
198    "this",
199    "throw",
200    "true",
201    "try",
202    "typeof",
203    "uint",
204    "ulong",
205    "unchecked",
206    "unsafe",
207    "ushort",
208    "using",
209    "virtual",
210    "void",
211    "volatile",
212    "while",
213];
214
215/// PHP reserved keywords.
216pub const PHP_KEYWORDS: &[&str] = &[
217    "abstract",
218    "and",
219    "as",
220    "break",
221    "callable",
222    "case",
223    "catch",
224    "class",
225    "clone",
226    "const",
227    "continue",
228    "declare",
229    "default",
230    "die",
231    "do",
232    "echo",
233    "else",
234    "elseif",
235    "empty",
236    "enddeclare",
237    "endfor",
238    "endforeach",
239    "endif",
240    "endswitch",
241    "endwhile",
242    "eval",
243    "exit",
244    "extends",
245    "final",
246    "finally",
247    "fn",
248    "for",
249    "foreach",
250    "function",
251    "global",
252    "goto",
253    "if",
254    "implements",
255    "include",
256    "instanceof",
257    "insteadof",
258    "interface",
259    "isset",
260    "list",
261    "match",
262    "namespace",
263    "new",
264    "or",
265    "print",
266    "private",
267    "protected",
268    "public",
269    "readonly",
270    "require",
271    "return",
272    "static",
273    "switch",
274    "throw",
275    "trait",
276    "try",
277    "unset",
278    "use",
279    "var",
280    "while",
281    "xor",
282    "yield",
283];
284
285/// Ruby reserved keywords.
286pub const RUBY_KEYWORDS: &[&str] = &[
287    "__ENCODING__",
288    "__FILE__",
289    "__LINE__",
290    "BEGIN",
291    "END",
292    "alias",
293    "and",
294    "begin",
295    "break",
296    "case",
297    "class",
298    "def",
299    "defined?",
300    "do",
301    "else",
302    "elsif",
303    "end",
304    "ensure",
305    "false",
306    "for",
307    "if",
308    "in",
309    "module",
310    "next",
311    "nil",
312    "not",
313    "or",
314    "redo",
315    "rescue",
316    "retry",
317    "return",
318    "self",
319    "super",
320    "then",
321    "true",
322    "undef",
323    "unless",
324    "until",
325    "when",
326    "while",
327    "yield",
328];
329
330/// Elixir reserved keywords (including sigil names and special atoms).
331pub const ELIXIR_KEYWORDS: &[&str] = &[
332    "after", "and", "catch", "do", "else", "end", "false", "fn", "in", "nil", "not", "or", "rescue", "true", "when",
333];
334
335/// Go reserved keywords.
336pub const GO_KEYWORDS: &[&str] = &[
337    "break",
338    "case",
339    "chan",
340    "const",
341    "continue",
342    "default",
343    "defer",
344    "else",
345    "fallthrough",
346    "for",
347    "func",
348    "go",
349    "goto",
350    "if",
351    "import",
352    "interface",
353    "map",
354    "package",
355    "range",
356    "return",
357    "select",
358    "struct",
359    "switch",
360    "type",
361    "var",
362];
363
364/// JavaScript / TypeScript reserved keywords (union of both).
365pub const JS_KEYWORDS: &[&str] = &[
366    "abstract",
367    "arguments",
368    "await",
369    "boolean",
370    "break",
371    "byte",
372    "case",
373    "catch",
374    "char",
375    "class",
376    "const",
377    "continue",
378    "debugger",
379    "default",
380    "delete",
381    "do",
382    "double",
383    "else",
384    "enum",
385    "eval",
386    "export",
387    "extends",
388    "false",
389    "final",
390    "finally",
391    "float",
392    "for",
393    "function",
394    "goto",
395    "if",
396    "implements",
397    "import",
398    "in",
399    "instanceof",
400    "int",
401    "interface",
402    "let",
403    "long",
404    "native",
405    "new",
406    "null",
407    "package",
408    "private",
409    "protected",
410    "public",
411    "return",
412    "short",
413    "static",
414    "super",
415    "switch",
416    "synchronized",
417    "this",
418    "throw",
419    "throws",
420    "transient",
421    "true",
422    "try",
423    "typeof",
424    "var",
425    "void",
426    "volatile",
427    "while",
428    "with",
429    "yield",
430];
431
432/// R reserved keywords.
433pub const R_KEYWORDS: &[&str] = &[
434    "FALSE", "Inf", "NA", "NaN", "NULL", "TRUE", "break", "else", "for", "function", "if", "in", "next", "repeat",
435    "return", "while",
436];
437
438/// Kotlin reserved keywords (hard + soft + modifier keywords that conflict with identifiers).
439pub const KOTLIN_KEYWORDS: &[&str] = &[
440    "as",
441    "break",
442    "class",
443    "continue",
444    "do",
445    "else",
446    "false",
447    "for",
448    "fun",
449    "if",
450    "in",
451    "interface",
452    "is",
453    "null",
454    "object",
455    "package",
456    "return",
457    "super",
458    "this",
459    "throw",
460    "true",
461    "try",
462    "typealias",
463    "typeof",
464    "val",
465    "var",
466    "when",
467    "while",
468    // Soft keywords commonly mistaken as identifiers
469    "by",
470    "init",
471    "constructor",
472    "field",
473    "value",
474    "where",
475];
476
477/// Swift reserved keywords (declarations + statements + expressions/types + patterns).
478pub const SWIFT_KEYWORDS: &[&str] = &[
479    "associatedtype",
480    "class",
481    "deinit",
482    "enum",
483    "extension",
484    "fileprivate",
485    "func",
486    "import",
487    "init",
488    "inout",
489    "internal",
490    "let",
491    "open",
492    "operator",
493    "private",
494    "protocol",
495    "public",
496    "rethrows",
497    "static",
498    "struct",
499    "subscript",
500    "typealias",
501    "var",
502    "break",
503    "case",
504    "continue",
505    "default",
506    "defer",
507    "do",
508    "else",
509    "fallthrough",
510    "for",
511    "guard",
512    "if",
513    "in",
514    "repeat",
515    "return",
516    "switch",
517    "where",
518    "while",
519    "as",
520    "Any",
521    "catch",
522    "false",
523    "is",
524    "nil",
525    "super",
526    "self",
527    "Self",
528    "throw",
529    "throws",
530    "true",
531    "try",
532    "_",
533];
534
535/// Dart reserved + built-in identifiers that cannot be used as plain identifiers.
536pub const DART_KEYWORDS: &[&str] = &[
537    "abstract",
538    "as",
539    "assert",
540    "async",
541    "await",
542    "break",
543    "case",
544    "catch",
545    "class",
546    "const",
547    "continue",
548    "covariant",
549    "default",
550    "deferred",
551    "do",
552    "dynamic",
553    "else",
554    "enum",
555    "export",
556    "extends",
557    "extension",
558    "external",
559    "factory",
560    "false",
561    "final",
562    "finally",
563    "for",
564    "Function",
565    "get",
566    "hide",
567    "if",
568    "implements",
569    "import",
570    "in",
571    "interface",
572    "is",
573    "late",
574    "library",
575    "mixin",
576    "new",
577    "null",
578    "of",
579    "on",
580    "operator",
581    "part",
582    "required",
583    "rethrow",
584    "return",
585    "set",
586    "show",
587    "static",
588    "super",
589    "switch",
590    "sync",
591    "this",
592    "throw",
593    "true",
594    "try",
595    "typedef",
596    "var",
597    "void",
598    "when",
599    "while",
600    "with",
601    "yield",
602];
603
604/// Gleam reserved keywords.
605pub const GLEAM_KEYWORDS: &[&str] = &[
606    "as",
607    "assert",
608    "auto",
609    "case",
610    "const",
611    "delegate",
612    "derive",
613    "echo",
614    "else",
615    "fn",
616    "if",
617    "implement",
618    "import",
619    "let",
620    "macro",
621    "opaque",
622    "panic",
623    "pub",
624    "test",
625    "todo",
626    "type",
627    "use",
628];
629
630/// Zig reserved keywords.
631pub const ZIG_KEYWORDS: &[&str] = &[
632    "addrspace",
633    "align",
634    "allowzero",
635    "and",
636    "anyframe",
637    "anytype",
638    "asm",
639    "async",
640    "await",
641    "break",
642    "callconv",
643    "catch",
644    "comptime",
645    "const",
646    "continue",
647    "defer",
648    "else",
649    "enum",
650    "errdefer",
651    "error",
652    "export",
653    "extern",
654    "fn",
655    "for",
656    "if",
657    "inline",
658    "linksection",
659    "noalias",
660    "noinline",
661    "nosuspend",
662    "or",
663    "orelse",
664    "packed",
665    "pub",
666    "resume",
667    "return",
668    "struct",
669    "suspend",
670    "switch",
671    "test",
672    "threadlocal",
673    "try",
674    "union",
675    "unreachable",
676    "usingnamespace",
677    "var",
678    "volatile",
679    "while",
680];
681
682/// Rust reserved keywords (strict, reserved, and weak keywords from all editions).
683///
684/// This list covers every identifier that cannot be used as a bare identifier in Rust
685/// source code.  When a serde-renamed field name (e.g. `"type"`) is used as a Rust
686/// function parameter or struct-literal field, it must be written as a raw identifier
687/// (`r#type`) to avoid a compile error.
688pub const RUST_KEYWORDS: &[&str] = &[
689    // Strict keywords
690    "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if", "impl", "in",
691    "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super",
692    "trait", "true", "type", "unsafe", "use", "where", "while", // Edition-2018+ keywords
693    "async", "await", "dyn",
694    // Reserved keywords (may not be valid today but are reserved for future use)
695    "abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof", "unsized", "virtual", "yield",
696    "try",
697];
698
699/// Escape a name that is a Rust keyword by prepending the raw-identifier prefix (`r#`).
700///
701/// Returns `Some("r#<name>")` when `name` is a Rust keyword, `None` otherwise.
702/// Use this when emitting a Rust identifier (function parameter, struct-literal field,
703/// local variable) whose text comes from an external source (e.g. a serde rename) and
704/// may coincide with a reserved word.
705///
706/// Note: PyO3 strips the `r#` prefix when deriving the Python-facing name, so a parameter
707/// declared as `r#type` is still exposed to Python as `type`.
708pub fn rust_raw_ident_safe(name: &str) -> Option<String> {
709    if RUST_KEYWORDS.contains(&name) {
710        Some(format!("r#{name}"))
711    } else {
712        None
713    }
714}
715
716/// Convenience: always returns a usable Rust identifier, escaping reserved keywords with
717/// the raw-identifier prefix (`r#`).
718pub fn rust_raw_ident(name: &str) -> String {
719    rust_raw_ident_safe(name).unwrap_or_else(|| name.to_string())
720}
721
722/// Returns `true` if `name` is a syntactically valid Rust identifier (ignoring whether
723/// it is a reserved keyword — use `rust_raw_ident` to handle keywords separately).
724///
725/// Valid identifiers start with a letter or `_` and contain only alphanumeric characters
726/// and `_`.  Names like `"self-harm"` or `"self-harm/intent"` (serde renames containing
727/// hyphens or slashes) are NOT valid identifiers and should fall back to the Rust field
728/// name instead of being used directly as parameter names.
729pub fn is_valid_rust_ident_chars(name: &str) -> bool {
730    if name.is_empty() {
731        return false;
732    }
733    let mut chars = name.chars();
734    let first = chars.next().expect("non-empty string has a first char");
735    if !first.is_alphabetic() && first != '_' {
736        return false;
737    }
738    chars.all(|c| c.is_alphanumeric() || c == '_')
739}
740
741/// Return the escaped field name for use in the generated binding of the given language,
742/// or `None` if the name is not reserved and no escaping is needed.
743///
744/// The escape strategy appends `_` to the name (e.g. `class` → `class_`).
745/// Call sites should use the returned value as the Rust field name in the binding struct
746/// and add language-appropriate attribute annotations to preserve the original name in
747/// the user-facing API.
748pub fn python_safe_name(name: &str) -> Option<String> {
749    if PYTHON_KEYWORDS.contains(&name) {
750        Some(format!("{name}_"))
751    } else {
752        None
753    }
754}
755
756/// Like `python_safe_name` but always returns a `String`, using the original when no
757/// escaping is needed. Convenience wrapper for call sites that always need a `String`.
758pub fn python_ident(name: &str) -> String {
759    python_safe_name(name).unwrap_or_else(|| name.to_string())
760}
761
762/// Returns `Some(escaped_name)` if `name` is either a Python reserved keyword
763/// OR a `str` instance method name that would shadow in a `StrEnum` context.
764///
765/// Use this for `StrEnum` variant names to prevent mypy [assignment] errors.
766/// Escaping appends a trailing underscore (e.g., `title` → `title_`).
767pub fn python_str_enum_safe_name(name: &str) -> Option<String> {
768    if PYTHON_KEYWORDS.contains(&name) || PYTHON_STR_METHODS.contains(&name) {
769        Some(format!("{name}_"))
770    } else {
771        None
772    }
773}
774
775/// Like `python_str_enum_safe_name` but always returns a `String`, using the original
776/// when no escaping is needed. Convenience wrapper for `StrEnum` variant names.
777pub fn python_str_enum_ident(name: &str) -> String {
778    python_str_enum_safe_name(name).unwrap_or_else(|| name.to_string())
779}
780
781/// Returns `Some(escaped_name)` if `name` is a Kotlin reserved keyword, else `None`.
782pub fn kotlin_safe_name(name: &str) -> Option<String> {
783    if KOTLIN_KEYWORDS.contains(&name) {
784        Some(format!("{name}_"))
785    } else {
786        None
787    }
788}
789
790/// Convenience: always returns a usable Kotlin identifier.
791pub fn kotlin_ident(name: &str) -> String {
792    kotlin_safe_name(name).unwrap_or_else(|| name.to_string())
793}
794
795/// Returns `Some(escaped_name)` if `name` is a Swift reserved keyword, else `None`.
796pub fn swift_safe_name(name: &str) -> Option<String> {
797    if SWIFT_KEYWORDS.contains(&name) {
798        Some(format!("{name}_"))
799    } else {
800        None
801    }
802}
803
804/// Convenience: always returns a usable Swift identifier.
805pub fn swift_ident(name: &str) -> String {
806    swift_safe_name(name).unwrap_or_else(|| name.to_string())
807}
808
809/// Returns `Some(backtick_escaped_name)` if `name` is a Swift reserved keyword,
810/// else `None`.
811///
812/// Use this for identifiers that appear in *emitted Swift source code* — enum
813/// cases, struct field names, function parameter labels — where the idiomatic
814/// escape for a keyword collision is `` `keyword` `` (backticks) rather than a
815/// trailing underscore. For identifiers on the Rust side of the swift-bridge
816/// boundary use [`swift_safe_name`] / [`swift_ident`] instead.
817pub fn swift_case_safe_name(name: &str) -> Option<String> {
818    if SWIFT_KEYWORDS.contains(&name) {
819        Some(format!("`{name}`"))
820    } else {
821        None
822    }
823}
824
825/// Convenience: always returns a usable Swift identifier for emitted Swift
826/// code, wrapping reserved keywords in backticks (`` `default` ``).
827///
828/// This is the Swift-idiomatic escape for keyword-collision identifiers in
829/// Swift source — distinct from [`swift_ident`], which appends a trailing
830/// underscore for use on the Rust side of the bridge.
831pub fn swift_case_ident(name: &str) -> String {
832    swift_case_safe_name(name).unwrap_or_else(|| name.to_string())
833}
834
835/// Returns `Some(escaped_name)` if `name` is a Dart reserved keyword, else `None`.
836pub fn dart_safe_name(name: &str) -> Option<String> {
837    if DART_KEYWORDS.contains(&name) {
838        Some(format!("{name}_"))
839    } else {
840        None
841    }
842}
843
844/// Convenience: always returns a usable Dart identifier.
845pub fn dart_ident(name: &str) -> String {
846    dart_safe_name(name).unwrap_or_else(|| name.to_string())
847}
848
849/// Returns `Some(escaped_name)` if `name` is a Gleam reserved keyword, else `None`.
850pub fn gleam_safe_name(name: &str) -> Option<String> {
851    if GLEAM_KEYWORDS.contains(&name) {
852        Some(format!("{name}_"))
853    } else {
854        None
855    }
856}
857
858/// Convenience: always returns a usable Gleam identifier.
859pub fn gleam_ident(name: &str) -> String {
860    gleam_safe_name(name).unwrap_or_else(|| name.to_string())
861}
862
863/// Returns `Some(escaped_name)` if `name` is a Zig reserved keyword, else `None`.
864pub fn zig_safe_name(name: &str) -> Option<String> {
865    if ZIG_KEYWORDS.contains(&name) {
866        Some(format!("{name}_"))
867    } else {
868        None
869    }
870}
871
872/// Convenience: always returns a usable Zig identifier.
873///
874/// Sanitizes the input so that it is a valid Zig identifier:
875///   1. Non-`[A-Za-z0-9_]` characters are replaced with `_` (so serde renames like
876///      `og:image` or `Content-Type` become `og_image` / `Content_Type`).
877///   2. A leading digit is prefixed with `_`.
878///   3. The result is then checked against Zig's reserved-word list and escaped
879///      with a trailing `_` if necessary.
880pub fn zig_ident(name: &str) -> String {
881    let mut sanitized = String::with_capacity(name.len() + 1);
882    for ch in name.chars() {
883        if ch.is_ascii_alphanumeric() || ch == '_' {
884            sanitized.push(ch);
885        } else {
886            sanitized.push('_');
887        }
888    }
889    if sanitized.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
890        sanitized.insert(0, '_');
891    }
892    zig_safe_name(&sanitized).unwrap_or(sanitized)
893}
894
895#[cfg(test)]
896mod tests {
897    use super::*;
898
899    #[test]
900    fn python_class_is_reserved() {
901        assert_eq!(python_safe_name("class"), Some("class_".to_string()));
902    }
903
904    #[test]
905    fn python_ordinary_name_is_none() {
906        assert_eq!(python_safe_name("layout_class"), None);
907    }
908
909    #[test]
910    fn python_ident_reserved() {
911        assert_eq!(python_ident("class"), "class_");
912    }
913
914    #[test]
915    fn python_ident_ordinary() {
916        assert_eq!(python_ident("layout_class"), "layout_class");
917    }
918
919    #[test]
920    fn kotlin_class_is_reserved() {
921        assert_eq!(kotlin_safe_name("class"), Some("class_".to_string()));
922        assert_eq!(kotlin_safe_name("fun"), Some("fun_".to_string()));
923        assert_eq!(kotlin_safe_name("ordinary"), None);
924        assert_eq!(kotlin_ident("typealias"), "typealias_");
925    }
926
927    #[test]
928    fn swift_init_is_reserved() {
929        assert_eq!(swift_safe_name("init"), Some("init_".to_string()));
930        assert_eq!(swift_safe_name("Self"), Some("Self_".to_string()));
931        assert_eq!(swift_safe_name("normal"), None);
932        assert_eq!(swift_ident("protocol"), "protocol_");
933    }
934
935    #[test]
936    fn swift_case_ident_backtick_escapes_reserved_keywords() {
937        // Backtick escape is the Swift-idiomatic form for keyword-collision
938        // identifiers in *emitted Swift code* (enum cases, struct fields,
939        // function parameter labels). Distinct from `swift_ident`, which
940        // emits a trailing-underscore form suitable for the Rust side of the
941        // bridge.
942        assert_eq!(swift_case_ident("default"), "`default`");
943        assert_eq!(swift_case_ident("protocol"), "`protocol`");
944        assert_eq!(swift_case_ident("init"), "`init`");
945        assert_eq!(swift_case_ident("Self"), "`Self`");
946        assert_eq!(swift_case_ident("Any"), "`Any`");
947        assert_eq!(swift_case_ident("class"), "`class`");
948        assert_eq!(swift_case_ident("inout"), "`inout`");
949        assert_eq!(swift_case_ident("rethrows"), "`rethrows`");
950        // Non-reserved identifiers pass through unchanged.
951        assert_eq!(swift_case_ident("gitHub"), "gitHub");
952        assert_eq!(swift_case_ident("normal"), "normal");
953        assert_eq!(swift_case_ident("dracula"), "dracula");
954    }
955
956    #[test]
957    fn swift_case_safe_name_returns_some_for_reserved() {
958        assert_eq!(swift_case_safe_name("default"), Some("`default`".to_string()));
959        assert_eq!(swift_case_safe_name("normal"), None);
960    }
961
962    #[test]
963    fn dart_async_is_reserved() {
964        assert_eq!(dart_safe_name("async"), Some("async_".to_string()));
965        assert_eq!(dart_safe_name("late"), Some("late_".to_string()));
966        assert_eq!(dart_safe_name("normal"), None);
967        assert_eq!(dart_ident("required"), "required_");
968    }
969
970    #[test]
971    fn gleam_pub_is_reserved() {
972        assert_eq!(gleam_safe_name("pub"), Some("pub_".to_string()));
973        assert_eq!(gleam_safe_name("opaque"), Some("opaque_".to_string()));
974        assert_eq!(gleam_safe_name("normal"), None);
975        assert_eq!(gleam_ident("type"), "type_");
976    }
977
978    #[test]
979    fn zig_comptime_is_reserved() {
980        assert_eq!(zig_safe_name("comptime"), Some("comptime_".to_string()));
981        assert_eq!(zig_safe_name("errdefer"), Some("errdefer_".to_string()));
982        assert_eq!(zig_safe_name("normal"), None);
983        assert_eq!(zig_ident("usingnamespace"), "usingnamespace_");
984    }
985
986    #[test]
987    fn python_keywords_covers_common_cases() {
988        for kw in &[
989            "def", "return", "yield", "pass", "import", "from", "type", "None", "True", "False",
990        ] {
991            assert!(
992                python_safe_name(kw).is_some(),
993                "expected {kw:?} to be a Python reserved keyword"
994            );
995        }
996    }
997
998    #[test]
999    fn python_str_enum_ident_escapes_str_methods() {
1000        // str method-name collisions must be escaped with trailing underscore
1001        assert_eq!(python_str_enum_ident("title"), "title_");
1002        assert_eq!(python_str_enum_ident("lower"), "lower_");
1003        assert_eq!(python_str_enum_ident("upper"), "upper_");
1004        assert_eq!(python_str_enum_ident("count"), "count_");
1005        assert_eq!(python_str_enum_ident("capitalize"), "capitalize_");
1006        assert_eq!(python_str_enum_ident("split"), "split_");
1007    }
1008
1009    #[test]
1010    fn python_str_enum_ident_escapes_python_keywords() {
1011        // Python keywords should still be escaped (del is a keyword, not a method)
1012        assert_eq!(python_str_enum_ident("del"), "del_");
1013        assert_eq!(python_str_enum_ident("class"), "class_");
1014        assert_eq!(python_str_enum_ident("return"), "return_");
1015    }
1016
1017    #[test]
1018    fn python_str_enum_ident_passes_through_ordinary_names() {
1019        // Names that are neither keywords nor str methods pass through unchanged
1020        assert_eq!(python_str_enum_ident("body"), "body");
1021        assert_eq!(python_str_enum_ident("div"), "div");
1022        assert_eq!(python_str_enum_ident("paragraph"), "paragraph");
1023    }
1024
1025    #[test]
1026    fn python_str_enum_safe_name_returns_some_for_reserved() {
1027        assert_eq!(python_str_enum_safe_name("title"), Some("title_".to_string()));
1028        assert_eq!(python_str_enum_safe_name("del"), Some("del_".to_string()));
1029    }
1030
1031    #[test]
1032    fn python_str_enum_safe_name_returns_none_for_ordinary() {
1033        assert_eq!(python_str_enum_safe_name("body"), None);
1034        assert_eq!(python_str_enum_safe_name("content"), None);
1035    }
1036
1037    #[test]
1038    fn rust_raw_ident_escapes_rust_keywords() {
1039        assert_eq!(rust_raw_ident("type"), "r#type");
1040        assert_eq!(rust_raw_ident("match"), "r#match");
1041        assert_eq!(rust_raw_ident("fn"), "r#fn");
1042        assert_eq!(rust_raw_ident("loop"), "r#loop");
1043        assert_eq!(rust_raw_ident("struct"), "r#struct");
1044        assert_eq!(rust_raw_ident("move"), "r#move");
1045        assert_eq!(rust_raw_ident("ref"), "r#ref");
1046        assert_eq!(rust_raw_ident("async"), "r#async");
1047    }
1048
1049    #[test]
1050    fn rust_raw_ident_passes_through_ordinary_names() {
1051        assert_eq!(rust_raw_ident("content"), "content");
1052        assert_eq!(rust_raw_ident("item_type"), "item_type");
1053        assert_eq!(rust_raw_ident("model"), "model");
1054    }
1055
1056    #[test]
1057    fn rust_raw_ident_safe_returns_some_for_keywords() {
1058        assert_eq!(rust_raw_ident_safe("type"), Some("r#type".to_string()));
1059        assert_eq!(rust_raw_ident_safe("fn"), Some("r#fn".to_string()));
1060    }
1061
1062    #[test]
1063    fn rust_raw_ident_safe_returns_none_for_ordinary() {
1064        assert_eq!(rust_raw_ident_safe("content"), None);
1065        assert_eq!(rust_raw_ident_safe("model"), None);
1066    }
1067
1068    #[test]
1069    fn is_valid_rust_ident_chars_accepts_valid_identifiers() {
1070        assert!(is_valid_rust_ident_chars("content"));
1071        assert!(is_valid_rust_ident_chars("self_harm"));
1072        assert!(is_valid_rust_ident_chars("_private"));
1073        assert!(is_valid_rust_ident_chars("type")); // keyword, but char-valid
1074        assert!(is_valid_rust_ident_chars("CamelCase"));
1075    }
1076
1077    #[test]
1078    fn is_valid_rust_ident_chars_rejects_invalid_identifiers() {
1079        assert!(!is_valid_rust_ident_chars("self-harm")); // hyphen
1080        assert!(!is_valid_rust_ident_chars("self-harm/intent")); // hyphen and slash
1081        assert!(!is_valid_rust_ident_chars("sexual/minors")); // slash
1082        assert!(!is_valid_rust_ident_chars("")); // empty
1083        assert!(!is_valid_rust_ident_chars("123abc")); // starts with digit
1084    }
1085}