Skip to main content

alef_e2e/
escape.rs

1//! Language-specific string escaping for e2e test code generation.
2
3/// Escape a string for embedding in a Python string literal.
4pub fn escape_python(s: &str) -> String {
5    s.replace('\\', "\\\\")
6        .replace('"', "\\\"")
7        .replace('\n', "\\n")
8        .replace('\r', "\\r")
9        .replace('\t', "\\t")
10}
11
12/// Escape a string for embedding in a Rust string literal.
13pub fn escape_rust(s: &str) -> String {
14    s.replace('\\', "\\\\")
15        .replace('"', "\\\"")
16        .replace('\n', "\\n")
17        .replace('\r', "\\r")
18        .replace('\t', "\\t")
19}
20
21/// Compute the number of # needed for a Rust raw string literal.
22pub fn raw_string_hashes(s: &str) -> usize {
23    let mut max_hashes = 0;
24    let mut current = 0;
25    let mut after_quote = false;
26    for ch in s.chars() {
27        if ch == '"' {
28            after_quote = true;
29            current = 0;
30        } else if ch == '#' && after_quote {
31            current += 1;
32            max_hashes = max_hashes.max(current);
33        } else {
34            after_quote = false;
35            current = 0;
36        }
37    }
38    max_hashes + 1
39}
40
41/// Format a string as a Rust raw string literal (r#"..."#).
42pub fn rust_raw_string(s: &str) -> String {
43    let hashes = raw_string_hashes(s);
44    let h: String = "#".repeat(hashes);
45    format!("r{h}\"{s}\"{h}")
46}
47
48/// Escape a string for embedding in a JavaScript/TypeScript double-quoted string literal.
49///
50/// `$` does not need escaping in double-quoted strings (only in template literals).
51/// Escaping it would produce `\$` which Biome flags as `noUselessEscapeInString`.
52pub fn escape_js(s: &str) -> String {
53    s.replace('\\', "\\\\")
54        .replace('"', "\\\"")
55        .replace('\n', "\\n")
56        .replace('\r', "\\r")
57        .replace('\t', "\\t")
58}
59
60/// Escape a string for embedding in a JavaScript/TypeScript template literal (backtick string).
61///
62/// Template literals interpolate `${...}` and use backtick delimiters, so both
63/// `` ` `` and `$` must be escaped to prevent unintended interpolation.
64pub fn escape_js_template(s: &str) -> String {
65    s.replace('\\', "\\\\")
66        .replace('`', "\\`")
67        .replace('$', "\\$")
68}
69
70/// Format a string as a Go string literal (backtick or quoted).
71pub fn go_string_literal(s: &str) -> String {
72    if !s.contains('`') {
73        format!("`{s}`")
74    } else {
75        format!("\"{}\"", escape_go(s))
76    }
77}
78
79/// Escape a string for embedding in a Go double-quoted string.
80pub fn escape_go(s: &str) -> String {
81    s.replace('\\', "\\\\")
82        .replace('"', "\\\"")
83        .replace('\n', "\\n")
84        .replace('\r', "\\r")
85        .replace('\t', "\\t")
86}
87
88/// Escape a string for embedding in a Java string literal.
89pub fn escape_java(s: &str) -> String {
90    s.replace('\\', "\\\\")
91        .replace('"', "\\\"")
92        .replace('\n', "\\n")
93        .replace('\r', "\\r")
94        .replace('\t', "\\t")
95}
96
97/// Escape a string for embedding in a C# string literal.
98pub fn escape_csharp(s: &str) -> String {
99    s.replace('\\', "\\\\")
100        .replace('"', "\\\"")
101        .replace('\n', "\\n")
102        .replace('\r', "\\r")
103        .replace('\t', "\\t")
104}
105
106/// Escape a string for embedding in a PHP string literal.
107pub fn escape_php(s: &str) -> String {
108    s.replace('\\', "\\\\")
109        .replace('"', "\\\"")
110        .replace('$', "\\$")
111        .replace('\n', "\\n")
112        .replace('\r', "\\r")
113        .replace('\t', "\\t")
114}
115
116/// Escape a string for embedding in a double-quoted Ruby string literal.
117pub fn escape_ruby(s: &str) -> String {
118    s.replace('\\', "\\\\")
119        .replace('"', "\\\"")
120        .replace('#', "\\#")
121        .replace('\n', "\\n")
122        .replace('\r', "\\r")
123        .replace('\t', "\\t")
124}
125
126/// Escape a string for embedding in a single-quoted Ruby string literal.
127/// Single-quoted Ruby strings only interpret `\\` and `\'`.
128pub fn escape_ruby_single(s: &str) -> String {
129    s.replace('\\', "\\\\").replace('\'', "\\'")
130}
131
132/// Returns true if the string needs double quotes (contains control characters
133/// that require escape sequences only available in double-quoted strings).
134pub fn ruby_needs_double_quotes(s: &str) -> bool {
135    s.contains('\n') || s.contains('\r') || s.contains('\t') || s.contains('\0')
136}
137
138/// Format a string as a Ruby literal, preferring single quotes.
139pub fn ruby_string_literal(s: &str) -> String {
140    if ruby_needs_double_quotes(s) {
141        format!("\"{}\"", escape_ruby(s))
142    } else {
143        format!("'{}'", escape_ruby_single(s))
144    }
145}
146
147/// Escape a string for embedding in an Elixir string literal.
148pub fn escape_elixir(s: &str) -> String {
149    s.replace('\\', "\\\\")
150        .replace('"', "\\\"")
151        .replace('#', "\\#")
152        .replace('\n', "\\n")
153        .replace('\r', "\\r")
154        .replace('\t', "\\t")
155}
156
157/// Escape a string for embedding in an R string literal.
158pub fn escape_r(s: &str) -> String {
159    s.replace('\\', "\\\\")
160        .replace('"', "\\\"")
161        .replace('\n', "\\n")
162        .replace('\r', "\\r")
163        .replace('\t', "\\t")
164}
165
166/// Escape a string for embedding in a C string literal.
167pub fn escape_c(s: &str) -> String {
168    s.replace('\\', "\\\\")
169        .replace('"', "\\\"")
170        .replace('\n', "\\n")
171        .replace('\r', "\\r")
172        .replace('\t', "\\t")
173}
174
175/// Sanitize an identifier for use as a test function name.
176/// Replaces non-alphanumeric characters with underscores, strips leading digits.
177pub fn sanitize_ident(s: &str) -> String {
178    let mut result = String::with_capacity(s.len());
179    for ch in s.chars() {
180        if ch.is_ascii_alphanumeric() || ch == '_' {
181            result.push(ch);
182        } else {
183            result.push('_');
184        }
185    }
186    // Strip leading digits
187    let trimmed = result.trim_start_matches(|c: char| c.is_ascii_digit());
188    if trimmed.is_empty() {
189        "_".to_string()
190    } else {
191        trimmed.to_string()
192    }
193}
194
195/// Convert a category name to a sanitized filename component.
196pub fn sanitize_filename(s: &str) -> String {
197    s.chars()
198        .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
199        .collect::<String>()
200        .to_lowercase()
201}
202
203/// Escape a string for embedding in a POSIX single-quoted shell string literal.
204///
205/// Wraps the string in single quotes and escapes embedded single quotes as `'\''`.
206/// Single-quoted shell strings treat every character literally except `'`, so
207/// no other escaping is needed.
208pub fn escape_shell(s: &str) -> String {
209    s.replace('\'', r"'\''")
210}