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/// Return the escaped field name for use in the generated binding of the given language,
683/// or `None` if the name is not reserved and no escaping is needed.
684///
685/// The escape strategy appends `_` to the name (e.g. `class` → `class_`).
686/// Call sites should use the returned value as the Rust field name in the binding struct
687/// and add language-appropriate attribute annotations to preserve the original name in
688/// the user-facing API.
689pub fn python_safe_name(name: &str) -> Option<String> {
690    if PYTHON_KEYWORDS.contains(&name) {
691        Some(format!("{name}_"))
692    } else {
693        None
694    }
695}
696
697/// Like `python_safe_name` but always returns a `String`, using the original when no
698/// escaping is needed. Convenience wrapper for call sites that always need a `String`.
699pub fn python_ident(name: &str) -> String {
700    python_safe_name(name).unwrap_or_else(|| name.to_string())
701}
702
703/// Returns `Some(escaped_name)` if `name` is either a Python reserved keyword
704/// OR a `str` instance method name that would shadow in a `StrEnum` context.
705///
706/// Use this for `StrEnum` variant names to prevent mypy [assignment] errors.
707/// Escaping appends a trailing underscore (e.g., `title` → `title_`).
708pub fn python_str_enum_safe_name(name: &str) -> Option<String> {
709    if PYTHON_KEYWORDS.contains(&name) || PYTHON_STR_METHODS.contains(&name) {
710        Some(format!("{name}_"))
711    } else {
712        None
713    }
714}
715
716/// Like `python_str_enum_safe_name` but always returns a `String`, using the original
717/// when no escaping is needed. Convenience wrapper for `StrEnum` variant names.
718pub fn python_str_enum_ident(name: &str) -> String {
719    python_str_enum_safe_name(name).unwrap_or_else(|| name.to_string())
720}
721
722/// Returns `Some(escaped_name)` if `name` is a Kotlin reserved keyword, else `None`.
723pub fn kotlin_safe_name(name: &str) -> Option<String> {
724    if KOTLIN_KEYWORDS.contains(&name) {
725        Some(format!("{name}_"))
726    } else {
727        None
728    }
729}
730
731/// Convenience: always returns a usable Kotlin identifier.
732pub fn kotlin_ident(name: &str) -> String {
733    kotlin_safe_name(name).unwrap_or_else(|| name.to_string())
734}
735
736/// Returns `Some(escaped_name)` if `name` is a Swift reserved keyword, else `None`.
737pub fn swift_safe_name(name: &str) -> Option<String> {
738    if SWIFT_KEYWORDS.contains(&name) {
739        Some(format!("{name}_"))
740    } else {
741        None
742    }
743}
744
745/// Convenience: always returns a usable Swift identifier.
746pub fn swift_ident(name: &str) -> String {
747    swift_safe_name(name).unwrap_or_else(|| name.to_string())
748}
749
750/// Returns `Some(backtick_escaped_name)` if `name` is a Swift reserved keyword,
751/// else `None`.
752///
753/// Use this for identifiers that appear in *emitted Swift source code* — enum
754/// cases, struct field names, function parameter labels — where the idiomatic
755/// escape for a keyword collision is `` `keyword` `` (backticks) rather than a
756/// trailing underscore. For identifiers on the Rust side of the swift-bridge
757/// boundary use [`swift_safe_name`] / [`swift_ident`] instead.
758pub fn swift_case_safe_name(name: &str) -> Option<String> {
759    if SWIFT_KEYWORDS.contains(&name) {
760        Some(format!("`{name}`"))
761    } else {
762        None
763    }
764}
765
766/// Convenience: always returns a usable Swift identifier for emitted Swift
767/// code, wrapping reserved keywords in backticks (`` `default` ``).
768///
769/// This is the Swift-idiomatic escape for keyword-collision identifiers in
770/// Swift source — distinct from [`swift_ident`], which appends a trailing
771/// underscore for use on the Rust side of the bridge.
772pub fn swift_case_ident(name: &str) -> String {
773    swift_case_safe_name(name).unwrap_or_else(|| name.to_string())
774}
775
776/// Returns `Some(escaped_name)` if `name` is a Dart reserved keyword, else `None`.
777pub fn dart_safe_name(name: &str) -> Option<String> {
778    if DART_KEYWORDS.contains(&name) {
779        Some(format!("{name}_"))
780    } else {
781        None
782    }
783}
784
785/// Convenience: always returns a usable Dart identifier.
786pub fn dart_ident(name: &str) -> String {
787    dart_safe_name(name).unwrap_or_else(|| name.to_string())
788}
789
790/// Returns `Some(escaped_name)` if `name` is a Gleam reserved keyword, else `None`.
791pub fn gleam_safe_name(name: &str) -> Option<String> {
792    if GLEAM_KEYWORDS.contains(&name) {
793        Some(format!("{name}_"))
794    } else {
795        None
796    }
797}
798
799/// Convenience: always returns a usable Gleam identifier.
800pub fn gleam_ident(name: &str) -> String {
801    gleam_safe_name(name).unwrap_or_else(|| name.to_string())
802}
803
804/// Returns `Some(escaped_name)` if `name` is a Zig reserved keyword, else `None`.
805pub fn zig_safe_name(name: &str) -> Option<String> {
806    if ZIG_KEYWORDS.contains(&name) {
807        Some(format!("{name}_"))
808    } else {
809        None
810    }
811}
812
813/// Convenience: always returns a usable Zig identifier.
814///
815/// Sanitizes the input so that it is a valid Zig identifier:
816///   1. Non-`[A-Za-z0-9_]` characters are replaced with `_` (so serde renames like
817///      `og:image` or `Content-Type` become `og_image` / `Content_Type`).
818///   2. A leading digit is prefixed with `_`.
819///   3. The result is then checked against Zig's reserved-word list and escaped
820///      with a trailing `_` if necessary.
821pub fn zig_ident(name: &str) -> String {
822    let mut sanitized = String::with_capacity(name.len() + 1);
823    for ch in name.chars() {
824        if ch.is_ascii_alphanumeric() || ch == '_' {
825            sanitized.push(ch);
826        } else {
827            sanitized.push('_');
828        }
829    }
830    if sanitized.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
831        sanitized.insert(0, '_');
832    }
833    zig_safe_name(&sanitized).unwrap_or(sanitized)
834}
835
836#[cfg(test)]
837mod tests {
838    use super::*;
839
840    #[test]
841    fn python_class_is_reserved() {
842        assert_eq!(python_safe_name("class"), Some("class_".to_string()));
843    }
844
845    #[test]
846    fn python_ordinary_name_is_none() {
847        assert_eq!(python_safe_name("layout_class"), None);
848    }
849
850    #[test]
851    fn python_ident_reserved() {
852        assert_eq!(python_ident("class"), "class_");
853    }
854
855    #[test]
856    fn python_ident_ordinary() {
857        assert_eq!(python_ident("layout_class"), "layout_class");
858    }
859
860    #[test]
861    fn kotlin_class_is_reserved() {
862        assert_eq!(kotlin_safe_name("class"), Some("class_".to_string()));
863        assert_eq!(kotlin_safe_name("fun"), Some("fun_".to_string()));
864        assert_eq!(kotlin_safe_name("ordinary"), None);
865        assert_eq!(kotlin_ident("typealias"), "typealias_");
866    }
867
868    #[test]
869    fn swift_init_is_reserved() {
870        assert_eq!(swift_safe_name("init"), Some("init_".to_string()));
871        assert_eq!(swift_safe_name("Self"), Some("Self_".to_string()));
872        assert_eq!(swift_safe_name("normal"), None);
873        assert_eq!(swift_ident("protocol"), "protocol_");
874    }
875
876    #[test]
877    fn swift_case_ident_backtick_escapes_reserved_keywords() {
878        // Backtick escape is the Swift-idiomatic form for keyword-collision
879        // identifiers in *emitted Swift code* (enum cases, struct fields,
880        // function parameter labels). Distinct from `swift_ident`, which
881        // emits a trailing-underscore form suitable for the Rust side of the
882        // bridge.
883        assert_eq!(swift_case_ident("default"), "`default`");
884        assert_eq!(swift_case_ident("protocol"), "`protocol`");
885        assert_eq!(swift_case_ident("init"), "`init`");
886        assert_eq!(swift_case_ident("Self"), "`Self`");
887        assert_eq!(swift_case_ident("Any"), "`Any`");
888        assert_eq!(swift_case_ident("class"), "`class`");
889        assert_eq!(swift_case_ident("inout"), "`inout`");
890        assert_eq!(swift_case_ident("rethrows"), "`rethrows`");
891        // Non-reserved identifiers pass through unchanged.
892        assert_eq!(swift_case_ident("gitHub"), "gitHub");
893        assert_eq!(swift_case_ident("normal"), "normal");
894        assert_eq!(swift_case_ident("dracula"), "dracula");
895    }
896
897    #[test]
898    fn swift_case_safe_name_returns_some_for_reserved() {
899        assert_eq!(swift_case_safe_name("default"), Some("`default`".to_string()));
900        assert_eq!(swift_case_safe_name("normal"), None);
901    }
902
903    #[test]
904    fn dart_async_is_reserved() {
905        assert_eq!(dart_safe_name("async"), Some("async_".to_string()));
906        assert_eq!(dart_safe_name("late"), Some("late_".to_string()));
907        assert_eq!(dart_safe_name("normal"), None);
908        assert_eq!(dart_ident("required"), "required_");
909    }
910
911    #[test]
912    fn gleam_pub_is_reserved() {
913        assert_eq!(gleam_safe_name("pub"), Some("pub_".to_string()));
914        assert_eq!(gleam_safe_name("opaque"), Some("opaque_".to_string()));
915        assert_eq!(gleam_safe_name("normal"), None);
916        assert_eq!(gleam_ident("type"), "type_");
917    }
918
919    #[test]
920    fn zig_comptime_is_reserved() {
921        assert_eq!(zig_safe_name("comptime"), Some("comptime_".to_string()));
922        assert_eq!(zig_safe_name("errdefer"), Some("errdefer_".to_string()));
923        assert_eq!(zig_safe_name("normal"), None);
924        assert_eq!(zig_ident("usingnamespace"), "usingnamespace_");
925    }
926
927    #[test]
928    fn python_keywords_covers_common_cases() {
929        for kw in &[
930            "def", "return", "yield", "pass", "import", "from", "type", "None", "True", "False",
931        ] {
932            assert!(
933                python_safe_name(kw).is_some(),
934                "expected {kw:?} to be a Python reserved keyword"
935            );
936        }
937    }
938
939    #[test]
940    fn python_str_enum_ident_escapes_str_methods() {
941        // str method-name collisions must be escaped with trailing underscore
942        assert_eq!(python_str_enum_ident("title"), "title_");
943        assert_eq!(python_str_enum_ident("lower"), "lower_");
944        assert_eq!(python_str_enum_ident("upper"), "upper_");
945        assert_eq!(python_str_enum_ident("count"), "count_");
946        assert_eq!(python_str_enum_ident("capitalize"), "capitalize_");
947        assert_eq!(python_str_enum_ident("split"), "split_");
948    }
949
950    #[test]
951    fn python_str_enum_ident_escapes_python_keywords() {
952        // Python keywords should still be escaped (del is a keyword, not a method)
953        assert_eq!(python_str_enum_ident("del"), "del_");
954        assert_eq!(python_str_enum_ident("class"), "class_");
955        assert_eq!(python_str_enum_ident("return"), "return_");
956    }
957
958    #[test]
959    fn python_str_enum_ident_passes_through_ordinary_names() {
960        // Names that are neither keywords nor str methods pass through unchanged
961        assert_eq!(python_str_enum_ident("body"), "body");
962        assert_eq!(python_str_enum_ident("div"), "div");
963        assert_eq!(python_str_enum_ident("paragraph"), "paragraph");
964    }
965
966    #[test]
967    fn python_str_enum_safe_name_returns_some_for_reserved() {
968        assert_eq!(python_str_enum_safe_name("title"), Some("title_".to_string()));
969        assert_eq!(python_str_enum_safe_name("del"), Some("del_".to_string()));
970    }
971
972    #[test]
973    fn python_str_enum_safe_name_returns_none_for_ordinary() {
974        assert_eq!(python_str_enum_safe_name("body"), None);
975        assert_eq!(python_str_enum_safe_name("content"), None);
976    }
977}