Skip to content

Commit 5158daf

Browse files
committed
fix: use line-based heuristic for auto-export detection (#39)
The naive .contains("export") check in register_handler() would false-positive on string literals (e.g. '<config mode="export">'), comments (e.g. // TODO: export data), and identifiers (e.g. exportPath), skipping auto-export and causing handler registration to fail. Replace with has_export_statement() which checks whether any source line starts with 'export' (after leading whitespace), correctly ignoring occurrences inside strings, comments, and variable names. Closes #39 Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent 890e313 commit 5158daf

File tree

2 files changed

+144
-4
lines changed

2 files changed

+144
-4
lines changed

src/hyperlight-js-runtime/src/lib.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,15 @@ impl JsRuntime {
155155
let handler_script = handler_script.into();
156156
let handler_pwd = handler_pwd.into();
157157

158-
// If the handler script doesn't already export the handler function, we export it for the user.
159-
// This is a convenience for the common case where the handler script is just a single file that defines
160-
// the handler function, without needing to explicitly export it.
161-
let handler_script = if !handler_script.contains("export") {
158+
// If the handler script doesn't already contain an ES export statement,
159+
// append one for the user. This is a convenience for the common case where
160+
// the handler script defines a handler function without explicitly exporting it.
161+
//
162+
// We check whether any line *starts* with `export` (after leading whitespace)
163+
// rather than using a naive `.contains("export")`, which would false-positive
164+
// on string literals (e.g. '<config mode="export">'), comments
165+
// (e.g. // TODO: export data), or identifiers (e.g. exportPath).
166+
let handler_script = if !has_export_statement(&handler_script) {
162167
format!("{}\nexport {{ handler }};", handler_script)
163168
} else {
164169
handler_script
@@ -315,6 +320,20 @@ fn make_handler_path(function_name: &str, handler_dir: &str) -> String {
315320
handler_path
316321
}
317322

323+
/// Returns `true` if the script contains an actual ES `export` statement
324+
/// (as opposed to the word "export" inside a string literal, comment, or
325+
/// identifier like `exportPath`).
326+
///
327+
/// The heuristic checks whether any source line begins with `export` (after
328+
/// optional leading whitespace). This avoids the false positives from a
329+
/// naive `.contains("export")` while staying `no_std`-compatible.
330+
fn has_export_statement(script: &str) -> bool {
331+
script.lines().any(|line| {
332+
let trimmed = line.trim_start();
333+
trimmed.starts_with("export ") || trimmed.starts_with("export{")
334+
})
335+
}
336+
318337
// RAII guard that flushes the output buffer of libc when dropped.
319338
// This is used to make sure we flush all output after running a handler, without needing to manually call it in every code path.
320339
struct FlushGuard;

src/hyperlight-js/tests/handlers.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,124 @@ fn handle_event_rejects_empty_name() {
126126
"Error should mention empty name, got: {err}"
127127
);
128128
}
129+
130+
// ── Auto-export heuristic tests (issue #39) ──────────────────────────
131+
// The auto-export logic must only detect actual ES export statements,
132+
// not the word "export" inside string literals, comments, or identifiers.
133+
134+
#[test]
135+
fn handler_with_export_in_string_literal() {
136+
// "export" appears inside a string — auto-export should still fire
137+
let handler = Script::from_content(
138+
r#"
139+
function handler(event) {
140+
const xml = '<config mode="export">value</config>';
141+
return { result: xml };
142+
}
143+
"#,
144+
);
145+
146+
let proto = SandboxBuilder::new().build().unwrap();
147+
let mut sandbox = proto.load_runtime().unwrap();
148+
sandbox.add_handler("handler", handler).unwrap();
149+
let mut loaded = sandbox.get_loaded_sandbox().unwrap();
150+
151+
let res = loaded
152+
.handle_event("handler", "{}".to_string(), None)
153+
.unwrap();
154+
assert_eq!(
155+
res,
156+
r#"{"result":"<config mode=\"export\">value</config>"}"#
157+
);
158+
}
159+
160+
#[test]
161+
fn handler_with_export_in_comment() {
162+
// "export" appears in a comment — auto-export should still fire
163+
let handler = Script::from_content(
164+
r#"
165+
function handler(event) {
166+
// TODO: export this data to CSV
167+
return { result: 42 };
168+
}
169+
"#,
170+
);
171+
172+
let proto = SandboxBuilder::new().build().unwrap();
173+
let mut sandbox = proto.load_runtime().unwrap();
174+
sandbox.add_handler("handler", handler).unwrap();
175+
let mut loaded = sandbox.get_loaded_sandbox().unwrap();
176+
177+
let res = loaded
178+
.handle_event("handler", "{}".to_string(), None)
179+
.unwrap();
180+
assert_eq!(res, r#"{"result":42}"#);
181+
}
182+
183+
#[test]
184+
fn handler_with_export_in_identifier() {
185+
// "export" is part of an identifier — auto-export should still fire
186+
let handler = Script::from_content(
187+
r#"
188+
function handler(event) {
189+
const exportPath = "/tmp/out.csv";
190+
return { result: exportPath };
191+
}
192+
"#,
193+
);
194+
195+
let proto = SandboxBuilder::new().build().unwrap();
196+
let mut sandbox = proto.load_runtime().unwrap();
197+
sandbox.add_handler("handler", handler).unwrap();
198+
let mut loaded = sandbox.get_loaded_sandbox().unwrap();
199+
200+
let res = loaded
201+
.handle_event("handler", "{}".to_string(), None)
202+
.unwrap();
203+
assert_eq!(res, r#"{"result":"/tmp/out.csv"}"#);
204+
}
205+
206+
#[test]
207+
fn handler_with_explicit_export_is_not_doubled() {
208+
// Script already has an export statement — auto-export should be skipped
209+
let handler = Script::from_content(
210+
r#"
211+
function handler(event) {
212+
return { result: "explicit" };
213+
}
214+
export { handler };
215+
"#,
216+
);
217+
218+
let proto = SandboxBuilder::new().build().unwrap();
219+
let mut sandbox = proto.load_runtime().unwrap();
220+
sandbox.add_handler("handler", handler).unwrap();
221+
let mut loaded = sandbox.get_loaded_sandbox().unwrap();
222+
223+
let res = loaded
224+
.handle_event("handler", "{}".to_string(), None)
225+
.unwrap();
226+
assert_eq!(res, r#"{"result":"explicit"}"#);
227+
}
228+
229+
#[test]
230+
fn handler_with_export_default_function() {
231+
// `export function` — auto-export should be skipped
232+
let handler = Script::from_content(
233+
r#"
234+
export function handler(event) {
235+
return { result: "inline-export" };
236+
}
237+
"#,
238+
);
239+
240+
let proto = SandboxBuilder::new().build().unwrap();
241+
let mut sandbox = proto.load_runtime().unwrap();
242+
sandbox.add_handler("handler", handler).unwrap();
243+
let mut loaded = sandbox.get_loaded_sandbox().unwrap();
244+
245+
let res = loaded
246+
.handle_event("handler", "{}".to_string(), None)
247+
.unwrap();
248+
assert_eq!(res, r#"{"result":"inline-export"}"#);
249+
}

0 commit comments

Comments
 (0)