diff --git a/crates/neon-macros/src/export/function/mod.rs b/crates/neon-macros/src/export/function/mod.rs index 5ba8b57b1..651650f83 100644 --- a/crates/neon-macros/src/export/function/mod.rs +++ b/crates/neon-macros/src/export/function/mod.rs @@ -111,7 +111,10 @@ pub(super) fn export(meta: meta::Meta, input: syn::ItemFn) -> proc_macro::TokenS let export_name = meta .name .map(|name| quote::quote!(#name)) - .unwrap_or_else(|| quote::quote!(stringify!(#name))); + .unwrap_or_else(|| { + let name = to_camel_case(&name.to_string()); + quote::quote!(#name) + }); // Generate the function that is registered to create the function on addon initialization. // Braces are included to prevent names from polluting user code. @@ -386,3 +389,83 @@ fn check_this(opts: &meta::Meta, sig: &syn::Signature, has_context: bool) -> boo _ => false, } } + +// Convert identifiers to camel case with the following rules: +// * All leading and trailing underscores are preserved +// * All other underscores are removed +// * Characters immediately following a non-leading underscore are uppercased +// * Bail (no conversion) if an unexpected condition is encountered: +// - Uppercase character +// - More than one adjacent interior underscore +fn to_camel_case(name: &str) -> String { + let mut out = String::with_capacity(name.len()); + let mut it = name.chars(); + let mut next = it.next(); + let mut count = 0usize; + + // Keep leading underscores + while matches!(next, Some('_')) { + out.push('_'); + next = it.next(); + } + + // Convert to camel case + while let Some(c) = next { + match c { + // Keep a count for maintaining trailing underscores + '_' => count += 1, + + // Bail if there is an unexpected uppercase character or extra underscore + _ if c.is_uppercase() || count >= 2 => { + return name.to_string(); + } + + // Don't uppercase the middle of a word + _ if count == 0 => { + out.push(c); + count = 0; + } + + // Uppercase characters following an underscore + _ => { + out.extend(c.to_uppercase()); + count = 0; + } + } + + next = it.next(); + } + + // We don't know underscores are a suffix until iteration has completed; + // add them back. + for _ in 0..count { + out.push('_'); + } + + out +} + +#[cfg(test)] +mod test { + #[test] + fn to_camel_case() { + use super::to_camel_case; + + assert_eq!(to_camel_case(""), ""); + assert_eq!(to_camel_case("one"), "one"); + assert_eq!(to_camel_case("two_words"), "twoWords"); + assert_eq!(to_camel_case("three_word_name"), "threeWordName"); + assert_eq!(to_camel_case("extra__underscore"), "extra__underscore"); + assert_eq!(to_camel_case("PreserveCase"), "PreserveCase"); + assert_eq!(to_camel_case("PreServe_case"), "PreServe_case"); + assert_eq!(to_camel_case("_preserve_leading"), "_preserveLeading"); + assert_eq!(to_camel_case("__preserve_leading"), "__preserveLeading"); + assert_eq!(to_camel_case("preserve_trailing_"), "preserveTrailing_"); + assert_eq!(to_camel_case("preserve_trailing__"), "preserveTrailing__"); + assert_eq!(to_camel_case("_preserve_both_"), "_preserveBoth_"); + assert_eq!(to_camel_case("__preserve_both__"), "__preserveBoth__"); + assert_eq!(to_camel_case("_"), "_"); + assert_eq!(to_camel_case("__"), "__"); + assert_eq!(to_camel_case("___"), "___"); + } +} diff --git a/test/napi/lib/export.js b/test/napi/lib/export.js index 0af8f95f0..8deb8bbb6 100644 --- a/test/napi/lib/export.js +++ b/test/napi/lib/export.js @@ -22,16 +22,16 @@ function globals() { function functions() { it("void function", () => { - assert.strictEqual(addon.no_args_or_return(), undefined); + assert.strictEqual(addon.noArgsOrReturn(), undefined); }); it("add - sync", () => { - assert.strictEqual(addon.simple_add(1, 2), 3); + assert.strictEqual(addon.simpleAdd(1, 2), 3); assert.strictEqual(addon.renamedAdd(1, 2), 3); }); it("add - task", async () => { - const p1 = addon.add_task(1, 2); + const p1 = addon.addTask(1, 2); const p2 = addon.renamedAddTask(1, 2); assert.ok(p1 instanceof Promise); @@ -45,14 +45,14 @@ function functions() { const arr = ["b", "c", "a"]; const expected = [...arr].sort(); - assert.deepStrictEqual(addon.json_sort(arr), expected); + assert.deepStrictEqual(addon.jsonSort(arr), expected); assert.deepStrictEqual(addon.renamedJsonSort(arr), expected); }); it("json sort - task", async () => { const arr = ["b", "c", "a"]; const expected = [...arr].sort(); - const p1 = addon.json_sort_task(arr); + const p1 = addon.jsonSortTask(arr); const p2 = addon.renamedJsonSortTask(arr); assert.ok(p1 instanceof Promise); @@ -63,7 +63,7 @@ function functions() { }); it("can use context and handles", () => { - const actual = addon.concat_with_cx_and_handle("Hello,", " World!"); + const actual = addon.concatWithCxAndHandle("Hello,", " World!"); const expected = "Hello, World!"; assert.strictEqual(actual, expected); @@ -73,7 +73,7 @@ function functions() { const msg = "Oh, no!"; const expected = new Error(msg); - assert.throws(() => addon.fail_with_throw(msg), expected); + assert.throws(() => addon.failWithThrow(msg), expected); }); it("tasks are concurrent", async () => { @@ -81,12 +81,12 @@ function functions() { const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const start = process.hrtime.bigint(); - await Promise.all([addon.sleep_task(time), sleep(time)]); + await Promise.all([addon.sleepTask(time), sleep(time)]); const end = process.hrtime.bigint(); const duration = end - start; - // If `addon.sleep_task` blocks the thread, the tasks will run sequentially + // If `addon.sleepTask` blocks the thread, the tasks will run sequentially // and take a minimum of 2x `time`. Since they are run concurrently, we // expect the time to be closer to 1x `time`. const maxExpected = 2000000n * BigInt(time); @@ -95,6 +95,6 @@ function functions() { }); it("can use generic Cx in exported functions", () => { - assert.strictEqual(addon.number_with_cx(42), 42); + assert.strictEqual(addon.numberWithCx(42), 42); }); } diff --git a/test/napi/lib/extract.js b/test/napi/lib/extract.js index 35c0ef338..62926e3da 100644 --- a/test/napi/lib/extract.js +++ b/test/napi/lib/extract.js @@ -64,11 +64,11 @@ describe("Extractors", () => { }); it("Either", () => { - assert.strictEqual(addon.extract_either("hello"), "String: hello"); - assert.strictEqual(addon.extract_either(42), "Number: 42"); + assert.strictEqual(addon.extractEither("hello"), "String: hello"); + assert.strictEqual(addon.extractEither(42), "Number: 42"); assert.throws( - () => addon.extract_either({}), + () => addon.extractEither({}), (err) => { assert.match(err.message, /expected either.*String.*f64/); assert.match(err.left.message, /expected string/); diff --git a/test/napi/lib/futures.js b/test/napi/lib/futures.js index 2c3025a7e..d3e4558a9 100644 --- a/test/napi/lib/futures.js +++ b/test/napi/lib/futures.js @@ -58,29 +58,23 @@ describe("Futures", () => { describe("Exported Async Functions", () => { it("should be able to call `async fn`", async () => { - assert.strictEqual(await addon.async_fn_add(1, 2), 3); + assert.strictEqual(await addon.asyncFnAdd(1, 2), 3); }); it("should be able to call fn with async block", async () => { - assert.strictEqual(await addon.async_add(1, 2), 3); + assert.strictEqual(await addon.asyncAdd(1, 2), 3); }); it("should be able to call fallible `async fn`", async () => { - assert.strictEqual(await addon.async_fn_div(10, 2), 5); + assert.strictEqual(await addon.asyncFnDiv(10, 2), 5); - await assertRejects(() => addon.async_fn_div(10, 0), /Divide by zero/); - }); - - it("should be able to call fallible `async fn`", async () => { - assert.strictEqual(await addon.async_fn_div(10, 2), 5); - - await assertRejects(() => addon.async_fn_div(10, 0), /Divide by zero/); + await assertRejects(() => addon.asyncFnDiv(10, 0), /Divide by zero/); }); it("should be able to call fallible fn with async block", async () => { - assert.strictEqual(await addon.async_div(10, 2), 5); + assert.strictEqual(await addon.asyncDiv(10, 2), 5); - await assertRejects(() => addon.async_div(10, 0), /Divide by zero/); + await assertRejects(() => addon.asyncDiv(10, 0), /Divide by zero/); }); it("should be able to code on the event loop before and after async", async () => { @@ -100,7 +94,7 @@ describe("Futures", () => { process.on("async_with_events", eventHandler); try { - let res = await addon.async_with_events([ + let res = await addon.asyncWithEvents([ [1, 2], [3, 4], [5, 6], diff --git a/test/napi/src/js/export.rs b/test/napi/src/js/export.rs index 36f0b75c7..525332881 100644 --- a/test/napi/src/js/export.rs +++ b/test/napi/src/js/export.rs @@ -27,7 +27,7 @@ fn simple_add(a: f64, b: f64) -> f64 { } #[neon::export(name = "renamedAdd")] -fn renamed_add(a: f64, b: f64) -> f64 { +fn rs_renamed_add(a: f64, b: f64) -> f64 { simple_add(a, b) } @@ -37,7 +37,7 @@ fn add_task(a: f64, b: f64) -> f64 { } #[neon::export(task, name = "renamedAddTask")] -fn renamed_add_task(a: f64, b: f64) -> f64 { +fn rs_renamed_add_task(a: f64, b: f64) -> f64 { add_task(a, b) } @@ -48,7 +48,7 @@ fn json_sort(mut items: Vec) -> Vec { } #[neon::export(json, name = "renamedJsonSort")] -fn renamed_json_sort(items: Vec) -> Vec { +fn rs_renamed_json_sort(items: Vec) -> Vec { json_sort(items) } @@ -58,7 +58,7 @@ fn json_sort_task(items: Vec) -> Vec { } #[neon::export(json, name = "renamedJsonSortTask", task)] -fn renamed_json_sort_task(items: Vec) -> Vec { +fn rs_renamed_json_sort_task(items: Vec) -> Vec { json_sort(items) }