incurs_codemode/
normalize.rs1pub fn normalize_code(code: &str) -> String {
3 let source = strip_fence(code.trim()).trim();
4 if source.is_empty() {
5 return "async () => {}".to_string();
6 }
7 if is_arrow(source) {
8 return source.to_string();
9 }
10 if let Some(inner) = source.strip_prefix("export default ") {
11 return normalize_code(inner.trim_end_matches(';'));
12 }
13 if let Some(name) = single_function_name(source) {
14 return format!("async () => {{\n{source}\nreturn {name}();\n}}");
15 }
16 if let Some((before, expression)) = split_last_expression(source) {
17 return format!("async () => {{\n{before}return ({expression})\n}}");
18 }
19 format!("async () => {{\n{source}\n}}")
20}
21
22fn strip_fence(source: &str) -> &str {
23 let Some(after) = source.strip_prefix("```") else {
24 return source;
25 };
26 let Some(newline) = after.find('\n') else {
27 return source;
28 };
29 let body = &after[newline + 1..];
30 body.strip_suffix("```")
31 .map(str::trim_end)
32 .unwrap_or(source)
33}
34
35fn is_arrow(source: &str) -> bool {
36 let Some(arrow) = top_level_arrow(source) else {
37 return false;
38 };
39 let prefix = source[..arrow].trim();
40 prefix.ends_with(')') || prefix.starts_with("async ") || is_identifier(prefix)
41}
42
43fn top_level_arrow(source: &str) -> Option<usize> {
44 let mut depth = 0;
45 let mut quote = None;
46 let mut escaped = false;
47 let mut chars = source.char_indices().peekable();
48 while let Some((index, ch)) = chars.next() {
49 if let Some(active) = quote {
50 if escaped {
51 escaped = false;
52 } else if ch == '\\' {
53 escaped = true;
54 } else if ch == active {
55 quote = None;
56 }
57 continue;
58 }
59 match ch {
60 '\'' | '"' | '`' => quote = Some(ch),
61 '(' | '[' | '{' => depth += 1,
62 ')' | ']' | '}' => depth -= 1,
63 '=' if depth == 0 && chars.peek().is_some_and(|(_, next)| *next == '>') => {
64 return Some(index);
65 }
66 _ => {}
67 }
68 }
69 None
70}
71
72fn single_function_name(source: &str) -> Option<&str> {
73 let rest = source
74 .strip_prefix("async function ")
75 .or_else(|| source.strip_prefix("function "))?;
76 let end = rest.find('(')?;
77 let name = rest[..end].trim();
78 if is_identifier(name) && balanced(source) {
79 Some(name)
80 } else {
81 None
82 }
83}
84
85fn split_last_expression(source: &str) -> Option<(&str, &str)> {
86 if !balanced(source) || source.ends_with('}') {
87 return None;
88 }
89 let mut depth = 0;
90 let mut quote = None;
91 let mut escaped = false;
92 let mut split = None;
93 for (index, ch) in source.char_indices() {
94 if let Some(active) = quote {
95 if escaped {
96 escaped = false;
97 } else if ch == '\\' {
98 escaped = true;
99 } else if ch == active {
100 quote = None;
101 }
102 continue;
103 }
104 match ch {
105 '\'' | '"' | '`' => quote = Some(ch),
106 '(' | '[' | '{' => depth += 1,
107 ')' | ']' | '}' => depth -= 1,
108 ';' if depth == 0 => split = Some(index + 1),
109 _ => {}
110 }
111 }
112 let mut index = split.unwrap_or(0);
113 while source[index..]
114 .chars()
115 .next()
116 .is_some_and(char::is_whitespace)
117 {
118 index += source[index..].chars().next().unwrap().len_utf8();
119 }
120 let expression = source[index..].trim().trim_end_matches(';').trim();
121 if expression.is_empty() || starts_statement(expression) {
122 None
123 } else {
124 Some((&source[..index], expression))
125 }
126}
127
128fn starts_statement(source: &str) -> bool {
129 [
130 "const ",
131 "let ",
132 "var ",
133 "return ",
134 "throw ",
135 "if ",
136 "for ",
137 "while ",
138 "class ",
139 "function ",
140 "import ",
141 "export ",
142 "try ",
143 "switch ",
144 ]
145 .iter()
146 .any(|prefix| source.starts_with(prefix))
147}
148
149fn balanced(source: &str) -> bool {
150 let mut stack = Vec::new();
151 let mut quote = None;
152 let mut escaped = false;
153 for ch in source.chars() {
154 if let Some(active) = quote {
155 if escaped {
156 escaped = false;
157 } else if ch == '\\' {
158 escaped = true;
159 } else if ch == active {
160 quote = None;
161 }
162 continue;
163 }
164 match ch {
165 '\'' | '"' | '`' => quote = Some(ch),
166 '(' | '[' | '{' => stack.push(ch),
167 ')' if stack.pop() != Some('(') => return false,
168 ']' if stack.pop() != Some('[') => return false,
169 '}' if stack.pop() != Some('{') => return false,
170 _ => {}
171 }
172 }
173 stack.is_empty() && quote.is_none()
174}
175
176fn is_identifier(value: &str) -> bool {
177 let mut chars = value.chars();
178 chars
179 .next()
180 .is_some_and(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphabetic())
181 && chars.all(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphanumeric())
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn normalizes_common_model_outputs() {
190 assert_eq!(normalize_code(""), "async () => {}");
191 assert_eq!(normalize_code("async () => 1"), "async () => 1");
192 assert_eq!(
193 normalize_code("const x = 1;\nx + 2"),
194 "async () => {\nconst x = 1;\nreturn (x + 2)\n}"
195 );
196 assert_eq!(
197 normalize_code("```js\nstate.read({ id: 1 })\n```"),
198 "async () => {\nreturn (state.read({ id: 1 }))\n}"
199 );
200 assert_eq!(
201 normalize_code(
202 "const token = await codemode.step(\"token\", async () => \"stable\");\nreturn token;"
203 ),
204 "async () => {\nconst token = await codemode.step(\"token\", async () => \"stable\");\nreturn token;\n}"
205 );
206 }
207}