Skip to main content

spikard_cli/codegen/common/
identifier_sanitization.rs

1//! Language-specific identifier sanitization for code generation.
2//!
3//! Provides utilities to sanitize identifiers (variable names, class names, function names, etc.)
4//! for use in different programming languages. Handles reserved keywords, invalid characters,
5//! and language-specific naming conventions.
6//!
7//! # Examples
8//!
9//! ```
10//! use spikard_cli::codegen::common::identifier_sanitization::{sanitize_identifier, TargetLanguage};
11//!
12//! // Python reserved word becomes prefixed with underscore
13//! assert_eq!(
14//!     sanitize_identifier("class", TargetLanguage::Python),
15//!     "_class"
16//! );
17//!
18//! // Invalid characters are replaced with underscores
19//! assert_eq!(
20//!     sanitize_identifier("hello-world", TargetLanguage::TypeScript),
21//!     "hello_world"
22//! );
23//!
24//! // Identifiers starting with digits are prefixed
25//! assert_eq!(
26//!     sanitize_identifier("123field", TargetLanguage::Rust),
27//!     "_123field"
28//! );
29//! ```
30
31/// Target programming language for identifier sanitization.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum TargetLanguage {
34    /// Python 3.10+
35    Python,
36    /// TypeScript/JavaScript
37    TypeScript,
38    /// Rust 2024
39    Rust,
40    /// Ruby 3.2+
41    Ruby,
42    /// PHP 8.2+
43    Php,
44}
45
46impl TargetLanguage {
47    /// Get the reserved keywords for this language.
48    #[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    /// Get the reserved soft keywords that may be contextual for this language.
60    #[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    /// Get the keyword prefix for this language.
72    ///
73    /// When a reserved keyword is used as an identifier, it's prefixed with this character(s).
74    #[must_use]
75    pub const fn keyword_prefix(self) -> &'static str {
76        match self {
77            Self::Rust => "r#",
78            _ => "_",
79        }
80    }
81}
82
83/// Python 3.10+ reserved keywords.
84/// <https://docs.python.org/3/reference/lexical_analysis.html#keywords>
85const 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
91/// Python soft keywords (contextual).
92const PYTHON_SOFT_KEYWORDS: &[&str] = &["match", "case", "type"];
93
94/// TypeScript/JavaScript reserved keywords and common built-ins.
95/// <https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html>
96const 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
168/// TypeScript soft keywords.
169const TYPESCRIPT_SOFT_KEYWORDS: &[&str] = &["as", "require", "get", "set", "accessor"];
170
171/// Rust 2024 reserved keywords.
172/// <https://doc.rust-lang.org/reference/keywords.html>
173const 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
179/// Rust reserved but unused keywords (for future use).
180const RUST_SOFT_KEYWORDS: &[&str] = &[
181    "abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof", "unsized", "virtual", "yield",
182];
183
184/// Ruby 3.2+ reserved keywords.
185/// <https://ruby-doc.org/docs/ruby-doc-bundle/FAQ/FAQ.html#label-Keywords>
186const 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
230/// Ruby soft keywords.
231const RUBY_SOFT_KEYWORDS: &[&str] = &[];
232
233/// PHP 8.2+ reserved keywords.
234/// <https://www.php.net/manual/en/reserved.keywords.php>
235const 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
312/// PHP soft keywords.
313const PHP_SOFT_KEYWORDS: &[&str] = &["mixed", "object", "parent", "self", "static", "string", "void"];
314
315/// Sanitize an identifier to be valid in the target language.
316///
317/// This function:
318/// 1. Replaces invalid characters with underscores
319/// 2. Ensures the identifier doesn't start with a digit
320/// 3. Handles reserved keywords by prefixing with the language-specific prefix
321/// 4. Cleans up multiple consecutive underscores
322/// 5. Removes leading/trailing underscores (except when added as keyword prefix)
323///
324/// # Arguments
325///
326/// * `name` - The identifier to sanitize
327/// * `language` - The target programming language
328///
329/// # Returns
330///
331/// A sanitized identifier that is valid in the target language.
332///
333/// # Examples
334///
335/// ```
336/// use spikard_cli::codegen::common::identifier_sanitization::{sanitize_identifier, TargetLanguage};
337///
338/// // Reserved keyword gets prefixed
339/// assert_eq!(
340///     sanitize_identifier("def", TargetLanguage::Python),
341///     "_def"
342/// );
343///
344/// // Invalid characters replaced
345/// assert_eq!(
346///     sanitize_identifier("hello-world", TargetLanguage::Python),
347///     "hello_world"
348/// );
349///
350/// // Leading digits prefixed
351/// assert_eq!(
352///     sanitize_identifier("42answer", TargetLanguage::TypeScript),
353///     "_42answer"
354/// );
355/// ```
356#[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/// Sanitize an identifier and convert it to `snake_case`.
401///
402/// This is useful for Python, Ruby, and general use cases where `snake_case` is preferred.
403///
404/// # Arguments
405///
406/// * `name` - The identifier to sanitize
407/// * `language` - The target programming language
408///
409/// # Returns
410///
411/// A sanitized identifier in `snake_case` format.
412///
413/// # Examples
414///
415/// ```
416/// use spikard_cli::codegen::common::identifier_sanitization::{sanitize_identifier_snake_case, TargetLanguage};
417///
418/// assert_eq!(
419///     sanitize_identifier_snake_case("HelloWorld", TargetLanguage::Python),
420///     "hello_world"
421/// );
422/// ```
423#[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/// Sanitize an identifier and convert it to camelCase.
446///
447/// This is useful for TypeScript, JavaScript, and Java.
448///
449/// # Arguments
450///
451/// * `name` - The identifier to sanitize
452/// * `language` - The target programming language
453///
454/// # Returns
455///
456/// A sanitized identifier in camelCase format.
457///
458/// # Examples
459///
460/// ```
461/// use spikard_cli::codegen::common::identifier_sanitization::{sanitize_identifier_camel_case, TargetLanguage};
462///
463/// assert_eq!(
464///     sanitize_identifier_camel_case("hello_world", TargetLanguage::TypeScript),
465///     "helloWorld"
466/// );
467/// ```
468#[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/// Sanitize an identifier and convert it to `PascalCase`.
492///
493/// This is useful for class names and type names in most languages.
494///
495/// # Arguments
496///
497/// * `name` - The identifier to sanitize
498/// * `language` - The target programming language
499///
500/// # Returns
501///
502/// A sanitized identifier in `PascalCase` format.
503///
504/// # Examples
505///
506/// ```
507/// use spikard_cli::codegen::common::identifier_sanitization::{sanitize_identifier_pascal_case, TargetLanguage};
508///
509/// assert_eq!(
510///     sanitize_identifier_pascal_case("hello_world", TargetLanguage::Rust),
511///     "HelloWorld"
512/// );
513/// ```
514#[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}