#![cfg_attr(not(feature = "std"), no_std)]
mod ffi;
#[cfg(feature = "std")]
extern crate std;
use core::fmt;
use core::marker::PhantomData;
use core::num::NonZeroU32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LexerError {
EmptySource,
UnexpectedParen,
UnexpectedBrace,
UnterminatedParen,
UnterminatedBrace,
UnterminatedTemplateString,
UnterminatedStringLiteral,
UnterminatedRegexCharacterClass,
UnterminatedRegex,
UnexpectedEsmImportMeta,
UnexpectedEsmImport,
UnexpectedEsmExport,
TemplateNestOverflow,
Unknown(i32),
}
impl LexerError {
#[must_use]
pub fn from_code(code: i32) -> Self {
match code {
0 => Self::EmptySource,
1 => Self::UnexpectedParen,
2 => Self::UnexpectedBrace,
3 => Self::UnterminatedParen,
4 => Self::UnterminatedBrace,
5 => Self::UnterminatedTemplateString,
6 => Self::UnterminatedStringLiteral,
7 => Self::UnterminatedRegexCharacterClass,
8 => Self::UnterminatedRegex,
9 => Self::UnexpectedEsmImportMeta,
10 => Self::UnexpectedEsmImport,
11 => Self::UnexpectedEsmExport,
12 => Self::TemplateNestOverflow,
other => Self::Unknown(other),
}
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::EmptySource => "empty source",
Self::UnexpectedParen => "unexpected parenthesis",
Self::UnexpectedBrace => "unexpected brace",
Self::UnterminatedParen => "unterminated parenthesis",
Self::UnterminatedBrace => "unterminated brace",
Self::UnterminatedTemplateString => "unterminated template string",
Self::UnterminatedStringLiteral => "unterminated string literal",
Self::UnterminatedRegexCharacterClass => "unterminated regex character class",
Self::UnterminatedRegex => "unterminated regex",
Self::UnexpectedEsmImportMeta => "unexpected ESM import.meta",
Self::UnexpectedEsmImport => "unexpected ESM import",
Self::UnexpectedEsmExport => "unexpected ESM export",
Self::TemplateNestOverflow => "template nesting overflow",
Self::Unknown(_) => "unknown error",
}
}
}
impl fmt::Display for LexerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unknown(code) => write!(f, "merve lexer error: unknown (code {})", code),
_ => write!(f, "merve lexer error: {}", self.as_str()),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for LexerError {}
pub struct Analysis<'a> {
handle: ffi::merve_analysis,
_source: PhantomData<&'a [u8]>,
}
impl<'a> Drop for Analysis<'a> {
fn drop(&mut self) {
unsafe { ffi::merve_free(self.handle) }
}
}
unsafe impl Send for Analysis<'_> {}
unsafe impl Sync for Analysis<'_> {}
impl<'a> Analysis<'a> {
#[inline]
fn str_from_ffi(&self, s: ffi::merve_string) -> &str {
if s.length == 0 {
return "";
}
unsafe {
let slice = core::slice::from_raw_parts(s.data.cast(), s.length);
core::str::from_utf8_unchecked(slice)
}
}
#[must_use]
pub fn exports_count(&self) -> usize {
unsafe { ffi::merve_get_exports_count(self.handle) }
}
#[must_use]
pub fn reexports_count(&self) -> usize {
unsafe { ffi::merve_get_reexports_count(self.handle) }
}
#[must_use]
pub fn export_name(&self, index: usize) -> Option<&str> {
if index >= self.exports_count() {
return None;
}
let s = unsafe { ffi::merve_get_export_name(self.handle, index) };
Some(self.str_from_ffi(s))
}
#[must_use]
pub fn export_line(&self, index: usize) -> Option<NonZeroU32> {
if index >= self.exports_count() {
return None;
}
let line = unsafe { ffi::merve_get_export_line(self.handle, index) };
NonZeroU32::new(line)
}
#[must_use]
pub fn reexport_name(&self, index: usize) -> Option<&str> {
if index >= self.reexports_count() {
return None;
}
let s = unsafe { ffi::merve_get_reexport_name(self.handle, index) };
Some(self.str_from_ffi(s))
}
#[must_use]
pub fn reexport_line(&self, index: usize) -> Option<NonZeroU32> {
if index >= self.reexports_count() {
return None;
}
let line = unsafe { ffi::merve_get_reexport_line(self.handle, index) };
NonZeroU32::new(line)
}
#[must_use]
pub fn exports(&self) -> ExportIter<'a, '_> {
ExportIter {
analysis: self,
kind: ExportKind::Export,
index: 0,
count: self.exports_count(),
}
}
#[must_use]
pub fn reexports(&self) -> ExportIter<'a, '_> {
ExportIter {
analysis: self,
kind: ExportKind::ReExport,
index: 0,
count: self.reexports_count(),
}
}
}
impl fmt::Debug for Analysis<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Analysis")
.field("exports_count", &self.exports_count())
.field("reexports_count", &self.reexports_count())
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Export<'a> {
pub name: &'a str,
pub line: NonZeroU32,
}
impl fmt::Display for Export<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (line {})", self.name, self.line)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ExportKind {
Export,
ReExport,
}
pub struct ExportIter<'a, 'b> {
analysis: &'b Analysis<'a>,
kind: ExportKind,
index: usize,
count: usize,
}
impl<'a, 'b> Iterator for ExportIter<'a, 'b> {
type Item = Export<'b>;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.count {
return None;
}
let i = self.index;
self.index += 1;
let (name, line) = match self.kind {
ExportKind::Export => (
self.analysis
.export_name(i)
.expect("invariant: export index is in bounds"),
self.analysis
.export_line(i)
.expect("invariant: export line is non-zero and in bounds"),
),
ExportKind::ReExport => (
self.analysis
.reexport_name(i)
.expect("invariant: re-export index is in bounds"),
self.analysis
.reexport_line(i)
.expect("invariant: re-export line is non-zero and in bounds"),
),
};
Some(Export { name, line })
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.count - self.index;
(remaining, Some(remaining))
}
}
impl ExactSizeIterator for ExportIter<'_, '_> {}
pub fn parse_commonjs(source: &str) -> Result<Analysis<'_>, LexerError> {
if source.is_empty() {
return Err(LexerError::EmptySource);
}
let handle = unsafe { ffi::merve_parse_commonjs(source.as_ptr().cast(), source.len()) };
if handle.is_null() {
let code = unsafe { ffi::merve_get_last_error() };
return Err(if code >= 0 {
LexerError::from_code(code)
} else {
LexerError::Unknown(code)
});
}
if !unsafe { ffi::merve_is_valid(handle) } {
let code = unsafe { ffi::merve_get_last_error() };
let err = if code >= 0 {
LexerError::from_code(code)
} else {
LexerError::Unknown(code)
};
unsafe { ffi::merve_free(handle) };
return Err(err);
}
Ok(Analysis {
handle,
_source: PhantomData,
})
}
#[must_use]
pub fn version() -> &'static str {
unsafe {
let ptr = ffi::merve_get_version();
let len = {
let mut n = 0usize;
while *ptr.add(n) != 0 {
n += 1;
}
n
};
let slice = core::slice::from_raw_parts(ptr.cast(), len);
core::str::from_utf8_unchecked(slice)
}
}
#[must_use]
pub fn version_components() -> (i32, i32, i32) {
let v = unsafe { ffi::merve_get_version_components() };
(v.major, v.minor, v.revision)
}
#[cfg(test)]
mod tests {
use super::*;
use core::num::NonZeroU32;
#[test]
fn version_is_not_empty() {
let v = version();
assert!(!v.is_empty());
assert!(v.contains('.'), "version should contain a dot: {v}");
}
#[test]
fn version_components_are_nonnegative() {
let (major, minor, rev) = version_components();
assert!(major >= 0);
assert!(minor >= 0);
assert!(rev >= 0);
}
#[test]
fn parse_simple_exports() {
let source = "exports.foo = 1; exports.bar = 2;";
let analysis = parse_commonjs(source).expect("should parse");
assert_eq!(analysis.exports_count(), 2);
assert_eq!(analysis.export_name(0), Some("foo"));
assert_eq!(analysis.export_name(1), Some("bar"));
assert_eq!(analysis.reexports_count(), 0);
}
#[cfg(feature = "std")]
#[test]
fn parse_module_exports() {
let source = "module.exports = { a, b, c };";
let analysis = parse_commonjs(source).expect("should parse");
assert_eq!(analysis.exports_count(), 3);
assert_eq!(analysis.export_name(0), Some("a"));
assert_eq!(analysis.export_name(1), Some("b"));
assert_eq!(analysis.export_name(2), Some("c"));
}
#[test]
fn parse_reexports() {
let source = r#"module.exports = require("./other");"#;
let analysis = parse_commonjs(source).expect("should parse");
assert_eq!(analysis.reexports_count(), 1);
assert_eq!(analysis.reexport_name(0), Some("./other"));
}
#[test]
fn esm_import_returns_error() {
let source = "import { foo } from 'bar';";
let result = parse_commonjs(source);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err, LexerError::UnexpectedEsmImport);
}
#[test]
fn esm_export_returns_error() {
let source = "export const x = 1;";
let result = parse_commonjs(source);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err, LexerError::UnexpectedEsmExport);
}
#[test]
fn empty_input() {
let result = parse_commonjs("");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), LexerError::EmptySource);
}
#[test]
fn out_of_bounds_returns_none() {
let source = "exports.x = 1;";
let analysis = parse_commonjs(source).expect("should parse");
assert_eq!(analysis.export_name(999), None);
assert_eq!(analysis.export_line(999), None);
assert_eq!(analysis.reexport_name(0), None);
assert_eq!(analysis.reexport_line(0), None);
}
#[test]
fn export_lines() {
let source = "exports.a = 1;\nexports.b = 2;\nexports.c = 3;";
let analysis = parse_commonjs(source).expect("should parse");
assert_eq!(analysis.export_line(0), NonZeroU32::new(1));
assert_eq!(analysis.export_line(1), NonZeroU32::new(2));
assert_eq!(analysis.export_line(2), NonZeroU32::new(3));
}
#[cfg(feature = "std")]
#[test]
fn exports_iterator() {
let source = "exports.x = 1; exports.y = 2;";
let analysis = parse_commonjs(source).expect("should parse");
let exports: Vec<Export<'_>> = analysis.exports().collect();
assert_eq!(exports.len(), 2);
assert_eq!(exports[0].name, "x");
assert_eq!(exports[1].name, "y");
}
#[test]
fn exports_iterator_exact_size() {
let source = "exports.a = 1; exports.b = 2; exports.c = 3;";
let analysis = parse_commonjs(source).expect("should parse");
let iter = analysis.exports();
assert_eq!(iter.len(), 3);
}
#[cfg(feature = "std")]
#[test]
fn reexports_iterator() {
let source = r#"module.exports = require("./a");"#;
let analysis = parse_commonjs(source).expect("should parse");
let reexports: Vec<Export<'_>> = analysis.reexports().collect();
assert_eq!(reexports.len(), 1);
assert_eq!(reexports[0].name, "./a");
}
#[cfg(feature = "std")]
#[test]
fn debug_impl() {
let source = "exports.z = 1;";
let analysis = parse_commonjs(source).expect("should parse");
let dbg = format!("{:?}", analysis);
assert!(dbg.contains("Analysis"));
assert!(dbg.contains("exports_count: 1"));
}
#[cfg(feature = "std")]
#[test]
fn export_display_impl() {
let e = Export {
name: "foo",
line: NonZeroU32::new(42).unwrap(),
};
assert_eq!(format!("{e}"), "foo (line 42)");
}
#[cfg(feature = "std")]
#[test]
fn error_display() {
let err = LexerError::UnexpectedEsmImport;
let s = format!("{err}");
assert!(s.contains("unexpected ESM import"), "got: {s}");
}
#[cfg(feature = "std")]
#[test]
fn error_display_unknown() {
let err = LexerError::Unknown(99);
let s = format!("{err}");
assert!(s.contains("99"), "got: {s}");
}
#[test]
fn error_from_code_roundtrip() {
for code in 0..=12 {
let err = LexerError::from_code(code);
assert_ne!(err, LexerError::Unknown(code));
}
assert_eq!(LexerError::from_code(999), LexerError::Unknown(999));
}
#[cfg(feature = "std")]
#[test]
fn error_is_std_error() {
fn assert_error<E: std::error::Error>() {}
assert_error::<LexerError>();
}
#[test]
fn bracket_notation_exports() {
let source = r#"exports["hello-world"] = 1;"#;
let analysis = parse_commonjs(source).expect("should parse");
assert_eq!(analysis.exports_count(), 1);
assert_eq!(analysis.export_name(0), Some("hello-world"));
}
#[test]
fn multiple_independent_parses() {
let src1 = "exports.a = 1;";
let src2 = "exports.b = 2;";
let a1 = parse_commonjs(src1).expect("should parse");
let a2 = parse_commonjs(src2).expect("should parse");
assert_eq!(a1.export_name(0), Some("a"));
assert_eq!(a2.export_name(0), Some("b"));
}
#[test]
fn send_and_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Analysis<'_>>();
assert_sync::<Analysis<'_>>();
}
}