Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/js_parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ lexer_impl_header! {

// deinit → Drop (see impl Drop below)

fn decode_escape_sequences(
pub fn decode_escape_sequences(
&mut self,
start: usize,
text: &[u8],
Expand Down
2 changes: 1 addition & 1 deletion src/js_parser/lexer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ fn NewLexer_(
this.comments_to_preserve_before.clearAndFree();
}

fn decodeEscapeSequences(lexer: *LexerType, start: usize, text: string, comptime BufType: type, buf_: *BufType) !void {
pub fn decodeEscapeSequences(lexer: *LexerType, start: usize, text: string, comptime BufType: type, buf_: *BufType) !void {
var buf = buf_.*;
defer buf_.* = buf;
if (comptime is_json) lexer.is_ascii_only = false;
Expand Down
51 changes: 47 additions & 4 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,25 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
return;
}

// Visit substitution values before calling the macro so that
// statically-known identifiers are resolved before `toJS`.
// Force constant folding (as the `.e_call` path does) so
// expressions like `"a" + "b"` collapse to a single literal
// that `toJS` can convert.
{
let old_ce = p.options.ignore_dce_annotations;
let old_fold = p.should_fold_typescript_constant_expressions;
p.options.ignore_dce_annotations = true;
p.should_fold_typescript_constant_expressions = true;

for part in e_.parts_mut().iter_mut() {
p.visit_expr(&mut part.value);
}

p.options.ignore_dce_annotations = old_ce;
p.should_fold_typescript_constant_expressions = old_fold;
}

p.macro_call_count += 1;
let name: &[u8] = macro_ref_data.name.unwrap_or_else(|| {
e_.tag
Expand All @@ -769,13 +788,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
&p.import_records.items()[macro_ref_data.import_record_id as usize];
(record.path.text, record.range)
};
let start_error_count = p.log().msgs.len();
// We must visit it to convert inline_identifiers and record usage
// Reborrow via the field-disjoint `Lexer::log()` accessor
// so `&p.lexer` and `&mut p.options` split cleanly under
// borrowck — Zig held two raw `*Log`.
let log = p.lexer.log();
let source = p.source;
let Ok(macro_result) = p
let macro_result = match p
.options
.macro_context
.as_deref_mut()
Expand All @@ -788,16 +808,39 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
record_range,
expr,
name,
)
else {
return;
) {
Ok(r) => r,
Err(err) => {
if err == bun_core::err!("MacroFailed") {
if p.log().msgs.len() == start_error_count {
p.log().add_error(
Some(p.source),
expr.loc,
b"macro threw exception",
);
}
} else {
p.log().add_error_fmt(
Some(p.source),
expr.loc,
format_args!("\"{}\" error in macro", err.name()),
);
}
return;
}
};

if !matches!(macro_result.data, Data::ETemplate(..)) {
*e = macro_result;
p.visit_expr(e);
return;
}

// The macro returned the original tagged template (e.g. because
// it threw or returned undefined at top level). Parts were
// already visited above, so skip the second visit below.
// `E.Template.fold` is a no-op when `tag != null`, so no fold here.
return;
}
}
}
Expand Down
38 changes: 36 additions & 2 deletions src/js_parser/visit/visit_expr.zig
Original file line number Diff line number Diff line change
Expand Up @@ -408,23 +408,57 @@ pub fn VisitExpr(
return p.newExpr(E.Undefined{}, expr.loc);
}

// Visit substitution values before calling the macro so that
// statically-known identifiers are resolved before `toJS`.
// Force constant folding (as the `.e_call` path does) so
// expressions like `"a" + "b"` collapse to a single literal
// that `toJS` can convert.
{
const old_ce = p.options.ignore_dce_annotations;
defer p.options.ignore_dce_annotations = old_ce;
const old_fold = p.should_fold_typescript_constant_expressions;
defer p.should_fold_typescript_constant_expressions = old_fold;
p.options.ignore_dce_annotations = true;
p.should_fold_typescript_constant_expressions = true;

for (e_.parts) |*part| {
part.value = p.visitExpr(part.value);
}
}
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.

p.macro_call_count += 1;
const name = macro_ref_data.name orelse e_.tag.?.data.e_dot.name;
const record = &p.import_records.items[macro_ref_data.import_record_id];
const start_error_count = p.log.msgs.items.len;
// We must visit it to convert inline_identifiers and record usage
const macro_result = (p.options.macro_context.call(
const macro_result = p.options.macro_context.call(
record.path.text,
p.source.path.sourceDir(),
p.log,
p.source,
record.range,
expr,
name,
) catch return expr);
) catch |err| {
if (err == error.MacroFailed) {
if (p.log.msgs.items.len == start_error_count) {
p.log.addError(p.source, expr.loc, "macro threw exception") catch unreachable;
}
} else {
p.log.addErrorFmt(p.source, expr.loc, p.allocator, "\"{s}\" error in macro", .{@errorName(err)}) catch unreachable;
}
return expr;
};

if (macro_result.data != .e_template) {
return p.visitExpr(macro_result);
}

// The macro returned the original tagged template (e.g. because
// it threw or returned undefined at top level). Parts were
// already visited above, so skip the second visit below.
// `E.Template.fold` is a no-op when `tag != null`, so no fold here.
return expr;
}
}
}
Expand Down
139 changes: 132 additions & 7 deletions src/js_parser_jsc/Macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,110 @@ impl<'a> Run<'a> {
}
}

/// Convert a single raw template segment into its "cooked" JS string value.
/// Per spec, segments with undecodable escape sequences evaluate to `undefined`
/// in the cooked array while still appearing verbatim in `.raw`.
fn cook_raw_template_segment(
bump: &bun_alloc::Arena,
global: &JSGlobalObject,
raw: &[u8],
) -> Result<JSValue, MacroError> {
// Fast path: no escapes, CR, or non-ASCII → cooked == raw.
if strings::index_of_any(raw, b"\\\r").is_none() && strings::first_non_ascii(raw).is_none() {
return Ok(jsc::bun_string_jsc::create_utf8_for_js(global, raw)?);
}

// The lexer's escape decoder computes diagnostic source positions relative
// to the opening quote and can subtract up to three widths from the cursor
// when reporting `\u{…}` errors, so give it a source with a short prefix so
// the arithmetic can't underflow. We don't surface these diagnostics
// anywhere; the log is only inspected to detect failure.
const PREFIX: &[u8] = b" `";
let mut wrapped = Vec::with_capacity(PREFIX.len() + raw.len() + 1);
wrapped.extend_from_slice(PREFIX);
wrapped.extend_from_slice(raw);
wrapped.push(b'`');

let mut throwaway_log = Log::new();
let throwaway_source = Source::init_path_string("", wrapped.as_slice());
let mut lexer =
js_parser::lexer::Lexer::init_without_reading(&mut throwaway_log, &throwaway_source, bump);

let mut buf: Vec<u16> = Vec::with_capacity(raw.len());
let text = &wrapped[PREFIX.len()..wrapped.len() - 1];
if lexer
.decode_escape_sequences(PREFIX.len() - 1, text, &mut buf)
.is_err()
{
// Invalid escape sequence in a tagged template → cooked value is undefined.
return Ok(JSValue::UNDEFINED);
}
drop(lexer);
if throwaway_log.has_errors() {
return Ok(JSValue::UNDEFINED);
}

if buf.is_empty() {
// A segment that is only a line continuation (`\<newline>`) cooks to "".
let empty = bun_core::String::EMPTY;
return Ok(jsc::bun_string_jsc::to_js(&empty, global)?);
}

let (mut out, chars) = bun_core::String::create_uninitialized_utf16(buf.len());
chars.copy_from_slice(&buf);
Ok(jsc::bun_string_jsc::transfer_to_js(&mut out, global)?)
}

/// Build the `TemplateStringsArray`-shaped first argument for a tagged template call.
fn make_template_strings_array(
bump: &bun_alloc::Arena,
global: &JSGlobalObject,
template: &E::Template,
) -> Result<JSValue, MacroError> {
let parts = template.parts();
let segment_count = 1 + parts.len();

let cooked_array = JSValue::create_empty_array(global, segment_count)?;
let _cooked_guard = cooked_array.protected();

let raw_array = JSValue::create_empty_array(global, segment_count)?;
let _raw_guard = raw_array.protected();

for i in 0..segment_count {
let contents = if i == 0 {
&template.head
} else {
&parts[i - 1].tail
};
match contents {
E::TemplateContents::Raw(raw) => {
let raw_bytes = raw.slice();
raw_array.put_index(
global,
i as u32,
jsc::bun_string_jsc::create_utf8_for_js(global, raw_bytes)?,
)?;
cooked_array.put_index(
global,
i as u32,
cook_raw_template_segment(bump, global, raw_bytes)?,
)?;
}
E::TemplateContents::Cooked(cooked) => {
// Tagged templates are parsed with raw contents, so this branch
// should not occur for macro callers. Fall back to using the
// cooked value for both arrays so we never panic.
let js_str = crate::expr_jsc::string_to_js(cooked, global)?;
raw_array.put_index(global, i as u32, js_str)?;
cooked_array.put_index(global, i as u32, js_str)?;
}
}
}

cooked_array.put(global, "raw", raw_array);
Ok(cooked_array)
}

impl Runner {
pub fn run(
macro_: &Macro,
Expand Down Expand Up @@ -1033,13 +1137,34 @@ impl Runner {
js_args.args[i] = value;
}
}
ExprData::ETemplate(_) => {
log.add_error_fmt(
Some(source),
caller.loc,
format_args!("template literal macro invocations are not supported"),
);
return Err(MacroError::MacroFailed);
ExprData::ETemplate(template) => {
// A tagged template `` fn`a${x}b${y}c` `` is invoked as
// `fn(strings, x, y)` where `strings` is a frozen array of the
// cooked string segments with a `.raw` property holding the
// raw segments.
let parts = template.parts();
let extra = usize::from(javascript_object != JSValue::ZERO);
js_args.args = vec![JSValue::ZERO; 1 + parts.len() + extra];
js_args.processed_len = 0;

let strings_array =
make_template_strings_array(bump, global_object, template)?;
strings_array.protect();
js_args.args[0] = strings_array;
js_args.processed_len = 1;

for (i, part) in parts.iter().enumerate() {
let value = match part.value.to_js(global_object) {
Ok(v) => v,
Err(e) => {
js_args.processed_len = 1 + i;
return Err(e.into());
}
};
value.protect();
js_args.args[1 + i] = value;
}
js_args.processed_len = 1 + parts.len() + extra;
}
_ => {
panic!("Unexpected caller type");
Expand Down
Loading
Loading