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/// Java reserved keywords (including all contextual/reserved identifiers).
25pub const JAVA_KEYWORDS: &[&str] = &[
26    "abstract",
27    "assert",
28    "boolean",
29    "break",
30    "byte",
31    "case",
32    "catch",
33    "char",
34    "class",
35    "const",
36    "continue",
37    "default",
38    "do",
39    "double",
40    "else",
41    "enum",
42    "extends",
43    "final",
44    "finally",
45    "float",
46    "for",
47    "goto",
48    "if",
49    "implements",
50    "import",
51    "instanceof",
52    "int",
53    "interface",
54    "long",
55    "native",
56    "new",
57    "package",
58    "private",
59    "protected",
60    "public",
61    "return",
62    "short",
63    "static",
64    "strictfp",
65    "super",
66    "switch",
67    "synchronized",
68    "this",
69    "throw",
70    "throws",
71    "transient",
72    "try",
73    "void",
74    "volatile",
75    "while",
76];
77
78/// C# reserved keywords.
79pub const CSHARP_KEYWORDS: &[&str] = &[
80    "abstract",
81    "as",
82    "base",
83    "bool",
84    "break",
85    "byte",
86    "case",
87    "catch",
88    "char",
89    "checked",
90    "class",
91    "const",
92    "continue",
93    "decimal",
94    "default",
95    "delegate",
96    "do",
97    "double",
98    "else",
99    "enum",
100    "event",
101    "explicit",
102    "extern",
103    "false",
104    "finally",
105    "fixed",
106    "float",
107    "for",
108    "foreach",
109    "goto",
110    "if",
111    "implicit",
112    "in",
113    "int",
114    "interface",
115    "internal",
116    "is",
117    "lock",
118    "long",
119    "namespace",
120    "new",
121    "null",
122    "object",
123    "operator",
124    "out",
125    "override",
126    "params",
127    "private",
128    "protected",
129    "public",
130    "readonly",
131    "ref",
132    "return",
133    "sbyte",
134    "sealed",
135    "short",
136    "sizeof",
137    "stackalloc",
138    "static",
139    "string",
140    "struct",
141    "switch",
142    "this",
143    "throw",
144    "true",
145    "try",
146    "typeof",
147    "uint",
148    "ulong",
149    "unchecked",
150    "unsafe",
151    "ushort",
152    "using",
153    "virtual",
154    "void",
155    "volatile",
156    "while",
157];
158
159/// PHP reserved keywords.
160pub const PHP_KEYWORDS: &[&str] = &[
161    "abstract",
162    "and",
163    "as",
164    "break",
165    "callable",
166    "case",
167    "catch",
168    "class",
169    "clone",
170    "const",
171    "continue",
172    "declare",
173    "default",
174    "die",
175    "do",
176    "echo",
177    "else",
178    "elseif",
179    "empty",
180    "enddeclare",
181    "endfor",
182    "endforeach",
183    "endif",
184    "endswitch",
185    "endwhile",
186    "eval",
187    "exit",
188    "extends",
189    "final",
190    "finally",
191    "fn",
192    "for",
193    "foreach",
194    "function",
195    "global",
196    "goto",
197    "if",
198    "implements",
199    "include",
200    "instanceof",
201    "insteadof",
202    "interface",
203    "isset",
204    "list",
205    "match",
206    "namespace",
207    "new",
208    "or",
209    "print",
210    "private",
211    "protected",
212    "public",
213    "readonly",
214    "require",
215    "return",
216    "static",
217    "switch",
218    "throw",
219    "trait",
220    "try",
221    "unset",
222    "use",
223    "var",
224    "while",
225    "xor",
226    "yield",
227];
228
229/// Ruby reserved keywords.
230pub const RUBY_KEYWORDS: &[&str] = &[
231    "__ENCODING__",
232    "__FILE__",
233    "__LINE__",
234    "BEGIN",
235    "END",
236    "alias",
237    "and",
238    "begin",
239    "break",
240    "case",
241    "class",
242    "def",
243    "defined?",
244    "do",
245    "else",
246    "elsif",
247    "end",
248    "ensure",
249    "false",
250    "for",
251    "if",
252    "in",
253    "module",
254    "next",
255    "nil",
256    "not",
257    "or",
258    "redo",
259    "rescue",
260    "retry",
261    "return",
262    "self",
263    "super",
264    "then",
265    "true",
266    "undef",
267    "unless",
268    "until",
269    "when",
270    "while",
271    "yield",
272];
273
274/// Elixir reserved keywords (including sigil names and special atoms).
275pub const ELIXIR_KEYWORDS: &[&str] = &[
276    "after", "and", "catch", "do", "else", "end", "false", "fn", "in", "nil", "not", "or", "rescue", "true", "when",
277];
278
279/// Go reserved keywords.
280pub const GO_KEYWORDS: &[&str] = &[
281    "break",
282    "case",
283    "chan",
284    "const",
285    "continue",
286    "default",
287    "defer",
288    "else",
289    "fallthrough",
290    "for",
291    "func",
292    "go",
293    "goto",
294    "if",
295    "import",
296    "interface",
297    "map",
298    "package",
299    "range",
300    "return",
301    "select",
302    "struct",
303    "switch",
304    "type",
305    "var",
306];
307
308/// JavaScript / TypeScript reserved keywords (union of both).
309pub const JS_KEYWORDS: &[&str] = &[
310    "abstract",
311    "arguments",
312    "await",
313    "boolean",
314    "break",
315    "byte",
316    "case",
317    "catch",
318    "char",
319    "class",
320    "const",
321    "continue",
322    "debugger",
323    "default",
324    "delete",
325    "do",
326    "double",
327    "else",
328    "enum",
329    "eval",
330    "export",
331    "extends",
332    "false",
333    "final",
334    "finally",
335    "float",
336    "for",
337    "function",
338    "goto",
339    "if",
340    "implements",
341    "import",
342    "in",
343    "instanceof",
344    "int",
345    "interface",
346    "let",
347    "long",
348    "native",
349    "new",
350    "null",
351    "package",
352    "private",
353    "protected",
354    "public",
355    "return",
356    "short",
357    "static",
358    "super",
359    "switch",
360    "synchronized",
361    "this",
362    "throw",
363    "throws",
364    "transient",
365    "true",
366    "try",
367    "typeof",
368    "var",
369    "void",
370    "volatile",
371    "while",
372    "with",
373    "yield",
374];
375
376/// R reserved keywords.
377pub const R_KEYWORDS: &[&str] = &[
378    "FALSE", "Inf", "NA", "NaN", "NULL", "TRUE", "break", "else", "for", "function", "if", "in", "next", "repeat",
379    "return", "while",
380];
381
382/// Kotlin reserved keywords (hard + soft + modifier keywords that conflict with identifiers).
383pub const KOTLIN_KEYWORDS: &[&str] = &[
384    "as",
385    "break",
386    "class",
387    "continue",
388    "do",
389    "else",
390    "false",
391    "for",
392    "fun",
393    "if",
394    "in",
395    "interface",
396    "is",
397    "null",
398    "object",
399    "package",
400    "return",
401    "super",
402    "this",
403    "throw",
404    "true",
405    "try",
406    "typealias",
407    "typeof",
408    "val",
409    "var",
410    "when",
411    "while",
412    // Soft keywords commonly mistaken as identifiers
413    "by",
414    "init",
415    "constructor",
416    "field",
417    "value",
418    "where",
419];
420
421/// Swift reserved keywords (declarations + statements + expressions/types + patterns).
422pub const SWIFT_KEYWORDS: &[&str] = &[
423    "associatedtype",
424    "class",
425    "deinit",
426    "enum",
427    "extension",
428    "fileprivate",
429    "func",
430    "import",
431    "init",
432    "inout",
433    "internal",
434    "let",
435    "open",
436    "operator",
437    "private",
438    "protocol",
439    "public",
440    "rethrows",
441    "static",
442    "struct",
443    "subscript",
444    "typealias",
445    "var",
446    "break",
447    "case",
448    "continue",
449    "default",
450    "defer",
451    "do",
452    "else",
453    "fallthrough",
454    "for",
455    "guard",
456    "if",
457    "in",
458    "repeat",
459    "return",
460    "switch",
461    "where",
462    "while",
463    "as",
464    "Any",
465    "catch",
466    "false",
467    "is",
468    "nil",
469    "super",
470    "self",
471    "Self",
472    "throw",
473    "throws",
474    "true",
475    "try",
476    "_",
477];
478
479/// Dart reserved + built-in identifiers that cannot be used as plain identifiers.
480pub const DART_KEYWORDS: &[&str] = &[
481    "abstract",
482    "as",
483    "assert",
484    "async",
485    "await",
486    "break",
487    "case",
488    "catch",
489    "class",
490    "const",
491    "continue",
492    "covariant",
493    "default",
494    "deferred",
495    "do",
496    "dynamic",
497    "else",
498    "enum",
499    "export",
500    "extends",
501    "extension",
502    "external",
503    "factory",
504    "false",
505    "final",
506    "finally",
507    "for",
508    "Function",
509    "get",
510    "hide",
511    "if",
512    "implements",
513    "import",
514    "in",
515    "interface",
516    "is",
517    "late",
518    "library",
519    "mixin",
520    "new",
521    "null",
522    "of",
523    "on",
524    "operator",
525    "part",
526    "required",
527    "rethrow",
528    "return",
529    "set",
530    "show",
531    "static",
532    "super",
533    "switch",
534    "sync",
535    "this",
536    "throw",
537    "true",
538    "try",
539    "typedef",
540    "var",
541    "void",
542    "when",
543    "while",
544    "with",
545    "yield",
546];
547
548/// Gleam reserved keywords.
549pub const GLEAM_KEYWORDS: &[&str] = &[
550    "as",
551    "assert",
552    "auto",
553    "case",
554    "const",
555    "delegate",
556    "derive",
557    "echo",
558    "else",
559    "fn",
560    "if",
561    "implement",
562    "import",
563    "let",
564    "macro",
565    "opaque",
566    "panic",
567    "pub",
568    "test",
569    "todo",
570    "type",
571    "use",
572];
573
574/// Zig reserved keywords.
575pub const ZIG_KEYWORDS: &[&str] = &[
576    "addrspace",
577    "align",
578    "allowzero",
579    "and",
580    "anyframe",
581    "anytype",
582    "asm",
583    "async",
584    "await",
585    "break",
586    "callconv",
587    "catch",
588    "comptime",
589    "const",
590    "continue",
591    "defer",
592    "else",
593    "enum",
594    "errdefer",
595    "error",
596    "export",
597    "extern",
598    "fn",
599    "for",
600    "if",
601    "inline",
602    "linksection",
603    "noalias",
604    "noinline",
605    "nosuspend",
606    "or",
607    "orelse",
608    "packed",
609    "pub",
610    "resume",
611    "return",
612    "struct",
613    "suspend",
614    "switch",
615    "test",
616    "threadlocal",
617    "try",
618    "union",
619    "unreachable",
620    "usingnamespace",
621    "var",
622    "volatile",
623    "while",
624];
625
626/// Return the escaped field name for use in the generated binding of the given language,
627/// or `None` if the name is not reserved and no escaping is needed.
628///
629/// The escape strategy appends `_` to the name (e.g. `class` → `class_`).
630/// Call sites should use the returned value as the Rust field name in the binding struct
631/// and add language-appropriate attribute annotations to preserve the original name in
632/// the user-facing API.
633pub fn python_safe_name(name: &str) -> Option<String> {
634    if PYTHON_KEYWORDS.contains(&name) {
635        Some(format!("{name}_"))
636    } else {
637        None
638    }
639}
640
641/// Like `python_safe_name` but always returns a `String`, using the original when no
642/// escaping is needed. Convenience wrapper for call sites that always need a `String`.
643pub fn python_ident(name: &str) -> String {
644    python_safe_name(name).unwrap_or_else(|| name.to_string())
645}
646
647/// Returns `Some(escaped_name)` if `name` is a Kotlin reserved keyword, else `None`.
648pub fn kotlin_safe_name(name: &str) -> Option<String> {
649    if KOTLIN_KEYWORDS.contains(&name) {
650        Some(format!("{name}_"))
651    } else {
652        None
653    }
654}
655
656/// Convenience: always returns a usable Kotlin identifier.
657pub fn kotlin_ident(name: &str) -> String {
658    kotlin_safe_name(name).unwrap_or_else(|| name.to_string())
659}
660
661/// Returns `Some(escaped_name)` if `name` is a Swift reserved keyword, else `None`.
662pub fn swift_safe_name(name: &str) -> Option<String> {
663    if SWIFT_KEYWORDS.contains(&name) {
664        Some(format!("{name}_"))
665    } else {
666        None
667    }
668}
669
670/// Convenience: always returns a usable Swift identifier.
671pub fn swift_ident(name: &str) -> String {
672    swift_safe_name(name).unwrap_or_else(|| name.to_string())
673}
674
675/// Returns `Some(backtick_escaped_name)` if `name` is a Swift reserved keyword,
676/// else `None`.
677///
678/// Use this for identifiers that appear in *emitted Swift source code* — enum
679/// cases, struct field names, function parameter labels — where the idiomatic
680/// escape for a keyword collision is `` `keyword` `` (backticks) rather than a
681/// trailing underscore. For identifiers on the Rust side of the swift-bridge
682/// boundary use [`swift_safe_name`] / [`swift_ident`] instead.
683pub fn swift_case_safe_name(name: &str) -> Option<String> {
684    if SWIFT_KEYWORDS.contains(&name) {
685        Some(format!("`{name}`"))
686    } else {
687        None
688    }
689}
690
691/// Convenience: always returns a usable Swift identifier for emitted Swift
692/// code, wrapping reserved keywords in backticks (`` `default` ``).
693///
694/// This is the Swift-idiomatic escape for keyword-collision identifiers in
695/// Swift source — distinct from [`swift_ident`], which appends a trailing
696/// underscore for use on the Rust side of the bridge.
697pub fn swift_case_ident(name: &str) -> String {
698    swift_case_safe_name(name).unwrap_or_else(|| name.to_string())
699}
700
701/// Returns `Some(escaped_name)` if `name` is a Dart reserved keyword, else `None`.
702pub fn dart_safe_name(name: &str) -> Option<String> {
703    if DART_KEYWORDS.contains(&name) {
704        Some(format!("{name}_"))
705    } else {
706        None
707    }
708}
709
710/// Convenience: always returns a usable Dart identifier.
711pub fn dart_ident(name: &str) -> String {
712    dart_safe_name(name).unwrap_or_else(|| name.to_string())
713}
714
715/// Returns `Some(escaped_name)` if `name` is a Gleam reserved keyword, else `None`.
716pub fn gleam_safe_name(name: &str) -> Option<String> {
717    if GLEAM_KEYWORDS.contains(&name) {
718        Some(format!("{name}_"))
719    } else {
720        None
721    }
722}
723
724/// Convenience: always returns a usable Gleam identifier.
725pub fn gleam_ident(name: &str) -> String {
726    gleam_safe_name(name).unwrap_or_else(|| name.to_string())
727}
728
729/// Returns `Some(escaped_name)` if `name` is a Zig reserved keyword, else `None`.
730pub fn zig_safe_name(name: &str) -> Option<String> {
731    if ZIG_KEYWORDS.contains(&name) {
732        Some(format!("{name}_"))
733    } else {
734        None
735    }
736}
737
738/// Convenience: always returns a usable Zig identifier.
739///
740/// Sanitizes the input so that it is a valid Zig identifier:
741///   1. Non-`[A-Za-z0-9_]` characters are replaced with `_` (so serde renames like
742///      `og:image` or `Content-Type` become `og_image` / `Content_Type`).
743///   2. A leading digit is prefixed with `_`.
744///   3. The result is then checked against Zig's reserved-word list and escaped
745///      with a trailing `_` if necessary.
746pub fn zig_ident(name: &str) -> String {
747    let mut sanitized = String::with_capacity(name.len() + 1);
748    for ch in name.chars() {
749        if ch.is_ascii_alphanumeric() || ch == '_' {
750            sanitized.push(ch);
751        } else {
752            sanitized.push('_');
753        }
754    }
755    if sanitized.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
756        sanitized.insert(0, '_');
757    }
758    zig_safe_name(&sanitized).unwrap_or(sanitized)
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764
765    #[test]
766    fn python_class_is_reserved() {
767        assert_eq!(python_safe_name("class"), Some("class_".to_string()));
768    }
769
770    #[test]
771    fn python_ordinary_name_is_none() {
772        assert_eq!(python_safe_name("layout_class"), None);
773    }
774
775    #[test]
776    fn python_ident_reserved() {
777        assert_eq!(python_ident("class"), "class_");
778    }
779
780    #[test]
781    fn python_ident_ordinary() {
782        assert_eq!(python_ident("layout_class"), "layout_class");
783    }
784
785    #[test]
786    fn kotlin_class_is_reserved() {
787        assert_eq!(kotlin_safe_name("class"), Some("class_".to_string()));
788        assert_eq!(kotlin_safe_name("fun"), Some("fun_".to_string()));
789        assert_eq!(kotlin_safe_name("ordinary"), None);
790        assert_eq!(kotlin_ident("typealias"), "typealias_");
791    }
792
793    #[test]
794    fn swift_init_is_reserved() {
795        assert_eq!(swift_safe_name("init"), Some("init_".to_string()));
796        assert_eq!(swift_safe_name("Self"), Some("Self_".to_string()));
797        assert_eq!(swift_safe_name("normal"), None);
798        assert_eq!(swift_ident("protocol"), "protocol_");
799    }
800
801    #[test]
802    fn swift_case_ident_backtick_escapes_reserved_keywords() {
803        // Backtick escape is the Swift-idiomatic form for keyword-collision
804        // identifiers in *emitted Swift code* (enum cases, struct fields,
805        // function parameter labels). Distinct from `swift_ident`, which
806        // emits a trailing-underscore form suitable for the Rust side of the
807        // bridge.
808        assert_eq!(swift_case_ident("default"), "`default`");
809        assert_eq!(swift_case_ident("protocol"), "`protocol`");
810        assert_eq!(swift_case_ident("init"), "`init`");
811        assert_eq!(swift_case_ident("Self"), "`Self`");
812        assert_eq!(swift_case_ident("Any"), "`Any`");
813        assert_eq!(swift_case_ident("class"), "`class`");
814        assert_eq!(swift_case_ident("inout"), "`inout`");
815        assert_eq!(swift_case_ident("rethrows"), "`rethrows`");
816        // Non-reserved identifiers pass through unchanged.
817        assert_eq!(swift_case_ident("gitHub"), "gitHub");
818        assert_eq!(swift_case_ident("normal"), "normal");
819        assert_eq!(swift_case_ident("dracula"), "dracula");
820    }
821
822    #[test]
823    fn swift_case_safe_name_returns_some_for_reserved() {
824        assert_eq!(swift_case_safe_name("default"), Some("`default`".to_string()));
825        assert_eq!(swift_case_safe_name("normal"), None);
826    }
827
828    #[test]
829    fn dart_async_is_reserved() {
830        assert_eq!(dart_safe_name("async"), Some("async_".to_string()));
831        assert_eq!(dart_safe_name("late"), Some("late_".to_string()));
832        assert_eq!(dart_safe_name("normal"), None);
833        assert_eq!(dart_ident("required"), "required_");
834    }
835
836    #[test]
837    fn gleam_pub_is_reserved() {
838        assert_eq!(gleam_safe_name("pub"), Some("pub_".to_string()));
839        assert_eq!(gleam_safe_name("opaque"), Some("opaque_".to_string()));
840        assert_eq!(gleam_safe_name("normal"), None);
841        assert_eq!(gleam_ident("type"), "type_");
842    }
843
844    #[test]
845    fn zig_comptime_is_reserved() {
846        assert_eq!(zig_safe_name("comptime"), Some("comptime_".to_string()));
847        assert_eq!(zig_safe_name("errdefer"), Some("errdefer_".to_string()));
848        assert_eq!(zig_safe_name("normal"), None);
849        assert_eq!(zig_ident("usingnamespace"), "usingnamespace_");
850    }
851
852    #[test]
853    fn python_keywords_covers_common_cases() {
854        for kw in &[
855            "def", "return", "yield", "pass", "import", "from", "type", "None", "True", "False",
856        ] {
857            assert!(
858                python_safe_name(kw).is_some(),
859                "expected {kw:?} to be a Python reserved keyword"
860            );
861        }
862    }
863}