From 85ab7558b8ea70837d9e2efcd26b01fb5fb0cf05 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Tue, 24 Feb 2026 15:46:15 -0500 Subject: [PATCH] feat: add C API --- .github/workflows/release.yml | 4 +- CMakeLists.txt | 2 +- README.md | 94 ++++++++- include/merve_c.h | 171 +++++++++++++++ singleheader/CMakeLists.txt | 1 + singleheader/amalgamate.py | 11 +- src/CMakeLists.txt | 4 +- src/lexer.cpp | 2 - src/merve_c.cpp | 108 ++++++++++ tests/CMakeLists.txt | 19 ++ tests/c_api_compile_test.c | 48 +++++ tests/c_api_tests.cpp | 384 ++++++++++++++++++++++++++++++++++ 12 files changed, 838 insertions(+), 10 deletions(-) create mode 100644 include/merve_c.h delete mode 100644 src/lexer.cpp create mode 100644 src/merve_c.cpp create mode 100644 tests/c_api_compile_test.c create mode 100644 tests/c_api_tests.cpp diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5e8bea..a1b4cbc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,9 +45,9 @@ jobs: - name: Create singleheader.zip run: | cd build/singleheader - zip singleheader.zip merve.h merve.cpp + zip singleheader.zip merve.h merve.cpp merve_c.cpp mv singleheader.zip ../../singleheader/ - cp merve.h merve.cpp ../../singleheader/ + cp merve.h merve.cpp merve_c.cpp ../../singleheader/ - name: Create release run: gh release upload "$RELEASE_TAG" singleheader/* diff --git a/CMakeLists.txt b/CMakeLists.txt index 63756e3..d01b969 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -131,7 +131,7 @@ if(NOT MERVE_COVERAGE AND NOT EMSCRIPTEN) endif() install( - FILES include/merve.h + FILES include/merve.h include/merve_c.h DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" COMPONENT merve_development ) diff --git a/README.md b/README.md index b74ecb0..5d8ac88 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A fast C++ lexer for extracting named exports from CommonJS modules. This librar - **Source Locations**: Each export includes a 1-based line number for tooling integration - **Unicode Support**: Properly unescapes JavaScript string literals including `\u{XXXX}` and surrogate pairs - **Optional SIMD Acceleration**: Can use [simdutf](https://github.com/simdutf/simdutf) for faster string operations +- **C API**: Full C interface (`merve_c.h`) for use from C, FFI, or other languages - **No Dependencies**: Single-header distribution available (simdutf is optional) - **Cross-Platform**: Works on Linux, macOS, and Windows @@ -31,6 +32,7 @@ target_link_libraries(your_target PRIVATE lexer::lexer) ### Single Header Copy `singleheader/merve.h` and `singleheader/merve.cpp` to your project. +The C API header `singleheader/merve_c.h` is also included in the distribution. ## Usage @@ -130,6 +132,95 @@ const std::optional& get_last_error(); Returns the last parse error, if any. +## C API + +merve provides a C API (`merve_c.h`) for use from C programs, FFI bindings, or any language that can call C functions. The C API is compiled into the merve library alongside the C++ implementation. + +### C API Usage + +```c +#include "merve_c.h" +#include + +int main(void) { + const char* source = "exports.foo = 1;\nexports.bar = 2;\n"; + + merve_analysis result = merve_parse_commonjs(source, strlen(source)); + + if (merve_is_valid(result)) { + size_t count = merve_get_exports_count(result); + printf("Found %zu exports:\n", count); + for (size_t i = 0; i < count; i++) { + merve_string name = merve_get_export_name(result, i); + uint32_t line = merve_get_export_line(result, i); + printf(" - %.*s (line %u)\n", (int)name.length, name.data, line); + } + } else { + printf("Parse error: %d\n", merve_get_last_error()); + } + + merve_free(result); + return 0; +} +``` + +Output: +``` +Found 2 exports: + - foo (line 1) + - bar (line 2) +``` + +### C API Reference + +#### Types + +| Type | Description | +|------|-------------| +| `merve_string` | Non-owning string reference (`data` + `length`). Not null-terminated. | +| `merve_analysis` | Opaque handle to a parse result. Must be freed with `merve_free()`. | +| `merve_version_components` | Struct with `major`, `minor`, `revision` fields. | + +#### Functions + +| Function | Description | +|----------|-------------| +| `merve_parse_commonjs(input, length)` | Parse CommonJS source. Returns a handle (NULL only on OOM). | +| `merve_is_valid(result)` | Check if parsing succeeded. NULL-safe. | +| `merve_free(result)` | Free a parse result. NULL-safe. | +| `merve_get_exports_count(result)` | Number of named exports found. | +| `merve_get_reexports_count(result)` | Number of re-export specifiers found. | +| `merve_get_export_name(result, index)` | Get export name at index. Returns `{NULL, 0}` on error. | +| `merve_get_export_line(result, index)` | Get 1-based line number of export. Returns 0 on error. | +| `merve_get_reexport_name(result, index)` | Get re-export specifier at index. Returns `{NULL, 0}` on error. | +| `merve_get_reexport_line(result, index)` | Get 1-based line number of re-export. Returns 0 on error. | +| `merve_get_last_error()` | Last error code (`MERVE_ERROR_*`), or -1 if no error. | +| `merve_get_version()` | Version string (e.g. `"1.0.1"`). | +| `merve_get_version_components()` | Version as `{major, minor, revision}`. | + +#### Error Constants + +| Constant | Value | Description | +|----------|-------|-------------| +| `MERVE_ERROR_UNEXPECTED_ESM_IMPORT` | 10 | Found ESM `import` declaration | +| `MERVE_ERROR_UNEXPECTED_ESM_EXPORT` | 11 | Found ESM `export` declaration | +| `MERVE_ERROR_UNEXPECTED_ESM_IMPORT_META` | 9 | Found `import.meta` | +| `MERVE_ERROR_UNTERMINATED_STRING_LITERAL` | 6 | Unclosed string literal | +| `MERVE_ERROR_UNTERMINATED_TEMPLATE_STRING` | 5 | Unclosed template literal | +| `MERVE_ERROR_UNTERMINATED_REGEX` | 8 | Unclosed regular expression | +| `MERVE_ERROR_UNEXPECTED_PAREN` | 1 | Unexpected `)` | +| `MERVE_ERROR_UNEXPECTED_BRACE` | 2 | Unexpected `}` | +| `MERVE_ERROR_UNTERMINATED_PAREN` | 3 | Unclosed `(` | +| `MERVE_ERROR_UNTERMINATED_BRACE` | 4 | Unclosed `{` | +| `MERVE_ERROR_TEMPLATE_NEST_OVERFLOW` | 12 | Template literal nesting too deep | + +#### Lifetime Rules + +- The `merve_analysis` handle must be freed with `merve_free()`. +- `merve_string` values returned by accessors are valid as long as the handle has not been freed. +- For exports backed by a `string_view` (most identifiers), the original source buffer must also remain valid. +- All functions are NULL-safe: passing NULL returns safe defaults (false, 0, `{NULL, 0}`). + ## Supported Patterns ### Direct Exports @@ -243,8 +334,7 @@ cmake --build . ### Running Tests ```bash -cmake --build . --target real_world_tests -./tests/real_world_tests +ctest --test-dir build ``` ### Build Options diff --git a/include/merve_c.h b/include/merve_c.h new file mode 100644 index 0000000..af4a9d7 --- /dev/null +++ b/include/merve_c.h @@ -0,0 +1,171 @@ +/** + * @file merve_c.h + * @brief Includes the C definitions for merve. This is a C file, not C++. + */ +#ifndef MERVE_C_H +#define MERVE_C_H + +#include +#include +#include + +/** + * @brief Non-owning string reference. + * + * The data pointer is NOT null-terminated. Always use the length field. + * + * The data is valid as long as: + * - The merve_analysis handle that produced it has not been freed. + * - For string_view-backed exports: the original source buffer is alive. + */ +typedef struct { + const char* data; + size_t length; +} merve_string; + +/** + * @brief Opaque handle to a CommonJS parse result. + * + * Created by merve_parse_commonjs(). Must be freed with merve_free(). + */ +typedef void* merve_analysis; + +/** + * @brief Version number components. + */ +typedef struct { + int major; + int minor; + int revision; +} merve_version_components; + +/* Error codes corresponding to lexer::lexer_error values. */ +#define MERVE_ERROR_TODO 0 +#define MERVE_ERROR_UNEXPECTED_PAREN 1 +#define MERVE_ERROR_UNEXPECTED_BRACE 2 +#define MERVE_ERROR_UNTERMINATED_PAREN 3 +#define MERVE_ERROR_UNTERMINATED_BRACE 4 +#define MERVE_ERROR_UNTERMINATED_TEMPLATE_STRING 5 +#define MERVE_ERROR_UNTERMINATED_STRING_LITERAL 6 +#define MERVE_ERROR_UNTERMINATED_REGEX_CHARACTER_CLASS 7 +#define MERVE_ERROR_UNTERMINATED_REGEX 8 +#define MERVE_ERROR_UNEXPECTED_ESM_IMPORT_META 9 +#define MERVE_ERROR_UNEXPECTED_ESM_IMPORT 10 +#define MERVE_ERROR_UNEXPECTED_ESM_EXPORT 11 +#define MERVE_ERROR_TEMPLATE_NEST_OVERFLOW 12 + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Parse CommonJS source code and extract export information. + * + * The source buffer must remain valid while accessing string_view-backed + * export names from the returned handle. + * + * You must call merve_free() on the returned handle when done. + * + * @param input Pointer to the JavaScript source (need not be null-terminated). + * NULL is treated as an empty string. + * @param length Length of the input in bytes. + * @return A handle to the parse result, or NULL on out-of-memory. + * Use merve_is_valid() to check if parsing succeeded. + */ +merve_analysis merve_parse_commonjs(const char* input, size_t length); + +/** + * Check whether the parse result is valid (parsing succeeded). + * + * @param result Handle returned by merve_parse_commonjs(). NULL returns false. + * @return true if parsing succeeded, false otherwise. + */ +bool merve_is_valid(merve_analysis result); + +/** + * Free a parse result and all associated memory. + * + * @param result Handle returned by merve_parse_commonjs(). NULL is a no-op. + */ +void merve_free(merve_analysis result); + +/** + * Get the number of named exports found. + * + * @param result A parse result handle. NULL returns 0. + * @return Number of exports, or 0 if result is NULL or invalid. + */ +size_t merve_get_exports_count(merve_analysis result); + +/** + * Get the number of re-export module specifiers found. + * + * @param result A parse result handle. NULL returns 0. + * @return Number of re-exports, or 0 if result is NULL or invalid. + */ +size_t merve_get_reexports_count(merve_analysis result); + +/** + * Get the name of an export at the given index. + * + * @param result A valid parse result handle. + * @param index Zero-based index (must be < merve_get_exports_count()). + * @return Non-owning string reference. Returns {NULL, 0} on error. + */ +merve_string merve_get_export_name(merve_analysis result, size_t index); + +/** + * Get the 1-based source line number of an export. + * + * @param result A valid parse result handle. + * @param index Zero-based index (must be < merve_get_exports_count()). + * @return 1-based line number, or 0 on error. + */ +uint32_t merve_get_export_line(merve_analysis result, size_t index); + +/** + * Get the module specifier of a re-export at the given index. + * + * @param result A valid parse result handle. + * @param index Zero-based index (must be < merve_get_reexports_count()). + * @return Non-owning string reference. Returns {NULL, 0} on error. + */ +merve_string merve_get_reexport_name(merve_analysis result, size_t index); + +/** + * Get the 1-based source line number of a re-export. + * + * @param result A valid parse result handle. + * @param index Zero-based index (must be < merve_get_reexports_count()). + * @return 1-based line number, or 0 on error. + */ +uint32_t merve_get_reexport_line(merve_analysis result, size_t index); + +/** + * Get the error code from the last merve_parse_commonjs() call. + * + * @return One of the MERVE_ERROR_* constants, or -1 if the last parse + * succeeded. + * @note This is global state, overwritten by each merve_parse_commonjs() call. + */ +int merve_get_last_error(void); + +/** + * Get the merve library version string. + * + * @return Null-terminated version string (e.g. "1.0.1"). Never NULL. + */ +const char* merve_get_version(void); + +/** + * Get the merve library version as individual components. + * + * @return Struct with major, minor, and revision fields. + */ +merve_version_components merve_get_version_components(void); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* MERVE_C_H */ diff --git a/singleheader/CMakeLists.txt b/singleheader/CMakeLists.txt index e9101c3..9698a6e 100644 --- a/singleheader/CMakeLists.txt +++ b/singleheader/CMakeLists.txt @@ -4,6 +4,7 @@ set(SINGLEHEADER_FILES ${CMAKE_CURRENT_BINARY_DIR}/merve.cpp ${CMAKE_CURRENT_BINARY_DIR}/merve.h + ${CMAKE_CURRENT_BINARY_DIR}/merve_c.h ) set_source_files_properties(${SINGLEHEADER_FILES} PROPERTIES GENERATED TRUE) diff --git a/singleheader/amalgamate.py b/singleheader/amalgamate.py index f0ea379..05c388a 100644 --- a/singleheader/amalgamate.py +++ b/singleheader/amalgamate.py @@ -34,7 +34,7 @@ AMALGAMATE_OUTPUT_PATH = os.environ["AMALGAMATE_OUTPUT_PATH"] # this list excludes the "src/generic headers" -ALLCFILES = ["parser.cpp"] +ALLCFILES = ["parser.cpp", "merve_c.cpp"] # order matters ALLCHEADERS = ["merve.h"] @@ -138,11 +138,20 @@ def dofile(fid: str, prepath: str, filename: str) -> None: amal_c.close() +# Copy merve_c.h to the output directory (it is already standalone). +MERVE_C_H_SRC = os.path.join(AMALGAMATE_INCLUDE_PATH, "merve_c.h") +MERVE_C_H_DST = os.path.join(AMALGAMATE_OUTPUT_PATH, "merve_c.h") +if os.path.exists(MERVE_C_H_SRC): + shutil.copy2(MERVE_C_H_SRC, MERVE_C_H_DST) + print(f"Copied {MERVE_C_H_SRC} to {MERVE_C_H_DST}") + zf = zipfile.ZipFile( os.path.join(AMALGAMATE_OUTPUT_PATH, "singleheader.zip"), "w", zipfile.ZIP_DEFLATED ) zf.write(os.path.join(AMALGAMATE_OUTPUT_PATH, OUTPUT_CPP), OUTPUT_CPP) zf.write(os.path.join(AMALGAMATE_OUTPUT_PATH, OUTPUT_H), OUTPUT_H) +if os.path.exists(MERVE_C_H_DST): + zf.write(MERVE_C_H_DST, "merve_c.h") print("Done with all files generation.") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a625ab8..4156149 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,9 +5,9 @@ message(STATUS "CMAKE_BUILD_TYPE : " ${CMAKE_BUILD_TYPE}) add_library(merve-include-source INTERFACE) target_include_directories(merve-include-source INTERFACE $) add_library(merve-source INTERFACE) -target_sources(merve-source INTERFACE $/parser.cpp) +target_sources(merve-source INTERFACE $/parser.cpp $/merve_c.cpp) target_link_libraries(merve-source INTERFACE merve-include-source) -add_library(merve parser.cpp) +add_library(merve parser.cpp merve_c.cpp) target_include_directories(merve PRIVATE $ ) target_include_directories(merve PUBLIC "$") diff --git a/src/lexer.cpp b/src/lexer.cpp deleted file mode 100644 index 4584537..0000000 --- a/src/lexer.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#include "merve.h" -#include "parser.cpp" diff --git a/src/merve_c.cpp b/src/merve_c.cpp new file mode 100644 index 0000000..1909fa3 --- /dev/null +++ b/src/merve_c.cpp @@ -0,0 +1,108 @@ +#include "merve.h" +#include "merve_c.h" + +#include + +struct merve_analysis_impl { + std::optional result{}; +}; + +static merve_string merve_string_create(const char* data, size_t length) { + merve_string out{}; + out.data = data; + out.length = length; + return out; +} + +extern "C" { + +merve_analysis merve_parse_commonjs(const char* input, size_t length) { + merve_analysis_impl* impl = new (std::nothrow) merve_analysis_impl(); + if (!impl) return nullptr; + if (input != nullptr) { + impl->result = lexer::parse_commonjs(std::string_view(input, length)); + } else { + impl->result = lexer::parse_commonjs(std::string_view("", 0)); + } + return static_cast(impl); +} + +bool merve_is_valid(merve_analysis result) { + if (!result) return false; + return static_cast(result)->result.has_value(); +} + +void merve_free(merve_analysis result) { + if (!result) return; + delete static_cast(result); +} + +size_t merve_get_exports_count(merve_analysis result) { + if (!result) return 0; + merve_analysis_impl* impl = static_cast(result); + if (!impl->result.has_value()) return 0; + return impl->result->exports.size(); +} + +size_t merve_get_reexports_count(merve_analysis result) { + if (!result) return 0; + merve_analysis_impl* impl = static_cast(result); + if (!impl->result.has_value()) return 0; + return impl->result->re_exports.size(); +} + +merve_string merve_get_export_name(merve_analysis result, size_t index) { + if (!result) return merve_string_create(nullptr, 0); + merve_analysis_impl* impl = static_cast(result); + if (!impl->result.has_value()) return merve_string_create(nullptr, 0); + if (index >= impl->result->exports.size()) + return merve_string_create(nullptr, 0); + std::string_view sv = + lexer::get_string_view(impl->result->exports[index]); + return merve_string_create(sv.data(), sv.size()); +} + +uint32_t merve_get_export_line(merve_analysis result, size_t index) { + if (!result) return 0; + merve_analysis_impl* impl = static_cast(result); + if (!impl->result.has_value()) return 0; + if (index >= impl->result->exports.size()) return 0; + return impl->result->exports[index].line; +} + +merve_string merve_get_reexport_name(merve_analysis result, size_t index) { + if (!result) return merve_string_create(nullptr, 0); + merve_analysis_impl* impl = static_cast(result); + if (!impl->result.has_value()) return merve_string_create(nullptr, 0); + if (index >= impl->result->re_exports.size()) + return merve_string_create(nullptr, 0); + std::string_view sv = + lexer::get_string_view(impl->result->re_exports[index]); + return merve_string_create(sv.data(), sv.size()); +} + +uint32_t merve_get_reexport_line(merve_analysis result, size_t index) { + if (!result) return 0; + merve_analysis_impl* impl = static_cast(result); + if (!impl->result.has_value()) return 0; + if (index >= impl->result->re_exports.size()) return 0; + return impl->result->re_exports[index].line; +} + +int merve_get_last_error(void) { + const std::optional& err = lexer::get_last_error(); + if (!err.has_value()) return -1; + return static_cast(err.value()); +} + +const char* merve_get_version(void) { return MERVE_VERSION; } + +merve_version_components merve_get_version_components(void) { + merve_version_components vc{}; + vc.major = lexer::MERVE_VERSION_MAJOR; + vc.minor = lexer::MERVE_VERSION_MINOR; + vc.revision = lexer::MERVE_VERSION_REVISION; + return vc; +} + +} /* extern "C" */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a1b1c9e..72c0549 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,6 +17,24 @@ else() target_link_libraries(real_world_tests PRIVATE GTest::gtest_main) gtest_discover_tests(real_world_tests) + add_executable(c_api_tests c_api_tests.cpp) + target_link_libraries(c_api_tests PRIVATE GTest::gtest_main) + gtest_discover_tests(c_api_tests) + + # Verify merve_c.h compiles as pure C (compile-only test). + add_executable(c_api_compile_test c_api_compile_test.c) + target_include_directories(c_api_compile_test PRIVATE ${PROJECT_SOURCE_DIR}/include) + set_target_properties(c_api_compile_test PROPERTIES + EXCLUDE_FROM_ALL TRUE + EXCLUDE_FROM_DEFAULT_BUILD TRUE + LINKER_LANGUAGE C + ) + add_test( + NAME c_api_compile_test + COMMAND ${CMAKE_COMMAND} --build . --target c_api_compile_test --config $ + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + ) + if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9) target_link_libraries(real_world_tests PUBLIC stdc++fs) @@ -25,6 +43,7 @@ else() if(MSVC OR MINGW) target_compile_definitions(real_world_tests PRIVATE _CRT_SECURE_NO_WARNINGS) + target_compile_definitions(c_api_tests PRIVATE _CRT_SECURE_NO_WARNINGS) endif() endif() diff --git a/tests/c_api_compile_test.c b/tests/c_api_compile_test.c new file mode 100644 index 0000000..77caaa1 --- /dev/null +++ b/tests/c_api_compile_test.c @@ -0,0 +1,48 @@ +/** + * @file c_api_compile_test.c + * @brief Verifies that merve_c.h compiles cleanly as pure C. + * + * This is a compile-only test. It does not call any functions; + * it merely checks that the types and declarations parse under a C compiler. + */ +#include "merve_c.h" + +/* Exercise every typedef so the compiler sees them. */ +static void check_types(void) { + merve_string s; + s.data = "hello"; + s.length = 5; + (void)s; + + merve_version_components vc; + vc.major = 1; + vc.minor = 0; + vc.revision = 1; + (void)vc; + + merve_analysis a = (merve_analysis)0; + (void)a; + + /* Verify the error constants are valid integer constant expressions. */ + int errors[] = { + MERVE_ERROR_TODO, + MERVE_ERROR_UNEXPECTED_PAREN, + MERVE_ERROR_UNEXPECTED_BRACE, + MERVE_ERROR_UNTERMINATED_PAREN, + MERVE_ERROR_UNTERMINATED_BRACE, + MERVE_ERROR_UNTERMINATED_TEMPLATE_STRING, + MERVE_ERROR_UNTERMINATED_STRING_LITERAL, + MERVE_ERROR_UNTERMINATED_REGEX_CHARACTER_CLASS, + MERVE_ERROR_UNTERMINATED_REGEX, + MERVE_ERROR_UNEXPECTED_ESM_IMPORT_META, + MERVE_ERROR_UNEXPECTED_ESM_IMPORT, + MERVE_ERROR_UNEXPECTED_ESM_EXPORT, + MERVE_ERROR_TEMPLATE_NEST_OVERFLOW, + }; + (void)errors; +} + +int main(void) { + check_types(); + return 0; +} diff --git a/tests/c_api_tests.cpp b/tests/c_api_tests.cpp new file mode 100644 index 0000000..cdcd0e4 --- /dev/null +++ b/tests/c_api_tests.cpp @@ -0,0 +1,384 @@ +#include "merve_c.h" + +#include "gtest/gtest.h" +#include + +// Helper: compare merve_string to a C string literal. +static bool merve_string_eq(merve_string s, const char* expected) { + size_t expected_len = std::strlen(expected); + if (s.length != expected_len) return false; + if (expected_len == 0) return true; + return std::memcmp(s.data, expected, expected_len) == 0; +} + +TEST(c_api_tests, version_string) { + const char* version = merve_get_version(); + ASSERT_NE(version, nullptr); + // Just verify it returns a non-empty string. + ASSERT_GT(std::strlen(version), 0u); +} + +TEST(c_api_tests, version_components) { + merve_version_components vc = merve_get_version_components(); + ASSERT_GE(vc.major, 1); + ASSERT_GE(vc.minor, 0); + ASSERT_GE(vc.revision, 0); +} + +TEST(c_api_tests, basic_exports) { + const char* source = "exports.foo = 1; exports.bar = 2;"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 2u); + ASSERT_EQ(merve_get_reexports_count(result), 0u); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 0), "foo")); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 1), "bar")); + merve_free(result); +} + +TEST(c_api_tests, module_exports_dot) { + const char* source = "module.exports.asdf = 'asdf';"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 1u); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 0), "asdf")); + merve_free(result); +} + +TEST(c_api_tests, module_exports_literal) { + const char* source = "module.exports = { a, b, c };"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 3u); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 0), "a")); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 1), "b")); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 2), "c")); + merve_free(result); +} + +TEST(c_api_tests, reexport) { + const char* source = "module.exports = require('./dep');"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 0u); + ASSERT_EQ(merve_get_reexports_count(result), 1u); + ASSERT_TRUE(merve_string_eq(merve_get_reexport_name(result, 0), "./dep")); + merve_free(result); +} + +TEST(c_api_tests, multiple_reexports) { + const char* source = + "__exportStar(require('a')); __exportStar(require('b'));"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_reexports_count(result), 2u); + ASSERT_TRUE(merve_string_eq(merve_get_reexport_name(result, 0), "a")); + ASSERT_TRUE(merve_string_eq(merve_get_reexport_name(result, 1), "b")); + merve_free(result); +} + +TEST(c_api_tests, esbuild_hint_style) { + const char* source = + "0 && (module.exports = {a, b, c}) && __exportStar(require('fs'));"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 3u); + ASSERT_EQ(merve_get_reexports_count(result), 1u); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 0), "a")); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 1), "b")); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 2), "c")); + ASSERT_TRUE(merve_string_eq(merve_get_reexport_name(result, 0), "fs")); + merve_free(result); +} + +TEST(c_api_tests, esm_import_error) { + const char* source = "import 'x';"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_FALSE(merve_is_valid(result)); + ASSERT_EQ(merve_get_last_error(), MERVE_ERROR_UNEXPECTED_ESM_IMPORT); + merve_free(result); +} + +TEST(c_api_tests, esm_export_error) { + const char* source = "export { x };"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_FALSE(merve_is_valid(result)); + ASSERT_EQ(merve_get_last_error(), MERVE_ERROR_UNEXPECTED_ESM_EXPORT); + merve_free(result); +} + +TEST(c_api_tests, import_meta_error) { + const char* source = "import.meta.url"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_FALSE(merve_is_valid(result)); + ASSERT_EQ(merve_get_last_error(), MERVE_ERROR_UNEXPECTED_ESM_IMPORT_META); + merve_free(result); +} + +TEST(c_api_tests, no_error_after_success) { + const char* source = "exports.x = 1;"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_last_error(), -1); + merve_free(result); +} + +TEST(c_api_tests, empty_input) { + merve_analysis result = merve_parse_commonjs("", 0); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 0u); + ASSERT_EQ(merve_get_reexports_count(result), 0u); + merve_free(result); +} + +TEST(c_api_tests, null_input) { + merve_analysis result = merve_parse_commonjs(NULL, 0); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 0u); + merve_free(result); +} + +TEST(c_api_tests, null_handle_safety) { + ASSERT_FALSE(merve_is_valid(NULL)); + ASSERT_EQ(merve_get_exports_count(NULL), 0u); + ASSERT_EQ(merve_get_reexports_count(NULL), 0u); + + merve_string s = merve_get_export_name(NULL, 0); + ASSERT_EQ(s.data, nullptr); + ASSERT_EQ(s.length, 0u); + ASSERT_EQ(merve_get_export_line(NULL, 0), 0u); + + s = merve_get_reexport_name(NULL, 0); + ASSERT_EQ(s.data, nullptr); + ASSERT_EQ(s.length, 0u); + ASSERT_EQ(merve_get_reexport_line(NULL, 0), 0u); + + merve_free(NULL); // must not crash +} + +TEST(c_api_tests, out_of_bounds_access) { + const char* source = "exports.x = 1;"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 1u); + + // Valid access + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 0), "x")); + ASSERT_NE(merve_get_export_line(result, 0), 0u); + + // Out of bounds exports + merve_string s = merve_get_export_name(result, 1); + ASSERT_EQ(s.data, nullptr); + ASSERT_EQ(s.length, 0u); + ASSERT_EQ(merve_get_export_line(result, 1), 0u); + + s = merve_get_export_name(result, 999); + ASSERT_EQ(s.data, nullptr); + ASSERT_EQ(s.length, 0u); + + // Out of bounds re-exports (none exist) + s = merve_get_reexport_name(result, 0); + ASSERT_EQ(s.data, nullptr); + ASSERT_EQ(s.length, 0u); + ASSERT_EQ(merve_get_reexport_line(result, 0), 0u); + + merve_free(result); +} + +TEST(c_api_tests, invalid_result_accessors) { + // Parse ESM to get an invalid result. + const char* source = "import 'x';"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_FALSE(merve_is_valid(result)); + + ASSERT_EQ(merve_get_exports_count(result), 0u); + ASSERT_EQ(merve_get_reexports_count(result), 0u); + + merve_string s = merve_get_export_name(result, 0); + ASSERT_EQ(s.data, nullptr); + ASSERT_EQ(s.length, 0u); + ASSERT_EQ(merve_get_export_line(result, 0), 0u); + + s = merve_get_reexport_name(result, 0); + ASSERT_EQ(s.data, nullptr); + ASSERT_EQ(s.length, 0u); + ASSERT_EQ(merve_get_reexport_line(result, 0), 0u); + + merve_free(result); +} + +TEST(c_api_tests, line_numbers) { + const char* source = + "// line 1\n" + "exports.a = 1;\n" + "\n" + "exports.b = 2;\n"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 2u); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 0), "a")); + ASSERT_EQ(merve_get_export_line(result, 0), 2u); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 1), "b")); + ASSERT_EQ(merve_get_export_line(result, 1), 4u); + merve_free(result); +} + +TEST(c_api_tests, reexport_line_numbers) { + const char* source = + "// line 1\n" + "module.exports = require('dep1');\n"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_reexports_count(result), 1u); + ASSERT_TRUE(merve_string_eq(merve_get_reexport_name(result, 0), "dep1")); + ASSERT_EQ(merve_get_reexport_line(result, 0), 2u); + merve_free(result); +} + +TEST(c_api_tests, bracket_notation_exports) { + const char* source = "exports['not identifier'] = 1;"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 1u); + ASSERT_TRUE( + merve_string_eq(merve_get_export_name(result, 0), "not identifier")); + merve_free(result); +} + +TEST(c_api_tests, define_property_exports) { + const char* source = + "Object.defineProperty(module.exports, 'thing', { value: true });\n" + "Object.defineProperty(exports, 'other', { enumerable: true, value: " + "true });"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 2u); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 0), "thing")); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 1), "other")); + merve_free(result); +} + +TEST(c_api_tests, whitespace_only) { + const char* source = " \n\t\r\n "; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 0u); + ASSERT_EQ(merve_get_reexports_count(result), 0u); + merve_free(result); +} + +TEST(c_api_tests, typescript_reexports) { + const char* source = + "\"use strict\";\n" + "__export(require(\"external1\"));\n" + "__exportStar(require(\"external2\"));\n"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_reexports_count(result), 2u); + ASSERT_TRUE( + merve_string_eq(merve_get_reexport_name(result, 0), "external1")); + ASSERT_TRUE( + merve_string_eq(merve_get_reexport_name(result, 1), "external2")); + merve_free(result); +} + +TEST(c_api_tests, shebang) { + const char* source = "#! ( {\n exports.asdf = 'asdf';\n "; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 1u); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 0), "asdf")); + merve_free(result); +} + +TEST(c_api_tests, conditional_exports) { + const char* source = + "if (condition) {\n" + " exports.a = 1;\n" + "} else {\n" + " exports.b = 2;\n" + "}\n"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 2u); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 0), "a")); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 1), "b")); + merve_free(result); +} + +TEST(c_api_tests, multiple_independent_parses) { + // Verify handles are independent. + const char* source1 = "exports.x = 1;"; + const char* source2 = "exports.y = 1; exports.z = 2;"; + + merve_analysis r1 = merve_parse_commonjs(source1, std::strlen(source1)); + merve_analysis r2 = merve_parse_commonjs(source2, std::strlen(source2)); + + ASSERT_NE(r1, nullptr); + ASSERT_NE(r2, nullptr); + ASSERT_NE(r1, r2); + + ASSERT_EQ(merve_get_exports_count(r1), 1u); + ASSERT_EQ(merve_get_exports_count(r2), 2u); + + ASSERT_TRUE(merve_string_eq(merve_get_export_name(r1, 0), "x")); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(r2, 0), "y")); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(r2, 1), "z")); + + merve_free(r1); + merve_free(r2); +} + +TEST(c_api_tests, non_identifiers) { + const char* source = + "module.exports = { 'ab cd': foo };\n" + "exports['@notidentifier'] = 'asdf';\n"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 2u); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 0), "ab cd")); + ASSERT_TRUE( + merve_string_eq(merve_get_export_name(result, 1), "@notidentifier")); + merve_free(result); +} + +TEST(c_api_tests, spread_reexports) { + const char* source = + "module.exports = {\n" + " ...require('dep1'),\n" + " name,\n" + " ...require('dep2'),\n" + "};\n"; + merve_analysis result = merve_parse_commonjs(source, std::strlen(source)); + ASSERT_NE(result, nullptr); + ASSERT_TRUE(merve_is_valid(result)); + ASSERT_EQ(merve_get_exports_count(result), 1u); + ASSERT_TRUE(merve_string_eq(merve_get_export_name(result, 0), "name")); + ASSERT_EQ(merve_get_reexports_count(result), 2u); + ASSERT_TRUE(merve_string_eq(merve_get_reexport_name(result, 0), "dep1")); + ASSERT_TRUE(merve_string_eq(merve_get_reexport_name(result, 1), "dep2")); + merve_free(result); +}