Skip to content

Commit

Permalink
feat: implement util.format, fix issue with console log when the util…
Browse files Browse the repository at this point in the history
….format pattern is used (#332)

Refs: #302
  • Loading branch information
fredbonin authored May 10, 2024
1 parent 982afe9 commit 8043098
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 17 deletions.
1 change: 1 addition & 0 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const ES_BUILD_OPTIONS = {
"xml",
"net",
"navigator",
"util",
"url",
"performance",
],
Expand Down
108 changes: 92 additions & 16 deletions llrt_core/src/modules/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ pub fn init(ctx: &Ctx<'_>) -> Result<()> {
Ok(())
}

const NEWLINE_LOOKUP: [char; 2] = [NEWLINE, CARRIAGE_RETURN];
pub const NEWLINE_LOOKUP: [char; 2] = [NEWLINE, CARRIAGE_RETURN];
const COLOR_RESET: &str = "\x1b[0m";
const COLOR_BLACK: &str = "\x1b[30m";
const COLOR_GREEN: &str = "\x1b[32m";
Expand Down Expand Up @@ -240,7 +240,7 @@ fn is_primitive_like_or_void(typeof_value: Type) -> bool {
}

#[inline(always)]
fn stringify_value<'js>(
pub fn stringify_value<'js>(
result: &mut String,
ctx: &Ctx<'js>,
obj: Value<'js>,
Expand Down Expand Up @@ -579,21 +579,30 @@ fn format_plain<'js>(ctx: Ctx<'js>, args: Rest<Value<'js>>) -> Result<String> {
format_values(&ctx, args, false)
}

fn format_values_internal<'js>(
pub fn format_values_internal<'js>(
result: &mut String,
ctx: &Ctx<'js>,
args: Rest<Value<'js>>,
tty: bool,
newline_char: char,
) -> Result<()> {
let mut write_space = false;
for arg in args.0.into_iter() {
if write_space {
result.push(' ');
// Parse arguments
let mut str_pattern_option_value: String = String::with_capacity(64);
let mut replacements = Vec::with_capacity(args.len() - 1);
for (index, arg) in args.0.into_iter().enumerate() {
if index == 0 {
stringify_value(&mut str_pattern_option_value, ctx, arg, tty, newline_char)?;
} else {
let mut result = String::with_capacity(64);
stringify_value(&mut result, ctx, arg, tty, newline_char)?;
replacements.push(result)
}
stringify_value(result, ctx, arg, tty, newline_char)?;
write_space = true
}

result.push_str(&format_string(
str_pattern_option_value.as_str(),
&replacements,
));
Ok(())
}

Expand All @@ -617,6 +626,63 @@ pub(crate) fn log_std_err<'js>(
write_log(stderr(), ctx, args, level)
}

fn format_string(format_str: &str, replacements: &[String]) -> String {
// Quick return if we don't have anything to replace
if replacements.is_empty() {
return format_str.to_owned();
}

// Define capacity
let mut capacity = format_str.len() + replacements.len(); // add the number of replacements in case we have more to account for spaces
for replacement in replacements {
capacity += replacement.len();
}
let mut result = String::with_capacity(capacity);
let mut replacement_idx = 0;

// Iterate over chars to find patterns
let mut chars = format_str.chars().peekable();
while let Some(current_char) = chars.next() {
if current_char == '%' {
if let Some(next_char) = chars.next() {
// Handle the case of %s, %d, etc, we keep it simple and only replaces string
let need_to_put_back_chars = match next_char {
's' | 'd' | 'i' | 'f' | 'o' | 'O' | 'j' => {
if replacement_idx < replacements.len() {
result.push_str(&replacements[replacement_idx]);
replacement_idx += 1;
false
} else {
true
}
},
'%' => {
// Handle the case of %% where we want to push only one %
result.push(current_char);
false
},
_ => true,
};
// Nothing was replaced, just add back what we found
if need_to_put_back_chars {
result.push(current_char);
result.push(next_char);
}
}
} else {
result.push(current_char);
}
}

// Add what remains
if replacement_idx < replacements.len() {
result.push(' ');
result.push_str(&replacements[replacement_idx..].join(" "));
}

result
}

#[allow(clippy::unused_io_amount)]
fn write_log<'js, T>(
mut output: T,
Expand Down Expand Up @@ -754,19 +820,29 @@ fn write_lambda_log<'js>(

let mut exception = None;

let mut write_space = false;
for arg in args.0.into_iter() {
if write_space {
values_string.push(' ');
}
// Parse arguments
let mut str_pattern_option_value: String = String::with_capacity(64);
let mut replacements = Vec::with_capacity(args.len() - 1);
for (index, arg) in args.0.into_iter().enumerate() {
if arg.is_error() && exception.is_none() {
let exception_value = arg.clone();
exception = Some(exception_value.into_exception().unwrap());
}
stringify_value(&mut values_string, ctx, arg, is_tty, NEWLINE)?;
write_space = true

if index == 0 {
stringify_value(&mut str_pattern_option_value, ctx, arg, is_tty, NEWLINE)?;
} else {
let mut result = String::with_capacity(64);
stringify_value(&mut result, ctx, arg, is_tty, NEWLINE)?;
replacements.push(result)
}
}

values_string.push_str(&format_string(
str_pattern_option_value.as_str(),
&replacements,
));

result.push_str(&escape_json(values_string.as_bytes()));
result.push('\"');
if let Some(exception) = exception {
Expand Down
15 changes: 14 additions & 1 deletion llrt_core/src/modules/util.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use rquickjs::function::{Func, Rest};
use rquickjs::{
cstr,
module::{Declarations, Exports, ModuleDef},
Ctx, Function, Result,
Ctx, Function, Result, Value,
};
use std::sync::atomic::Ordering;

use crate::modules::console::{format_values_internal, AWS_LAMBDA_MODE, NEWLINE_LOOKUP};
use crate::{module_builder::ModuleInfo, modules::module::export_default};

pub struct UtilModule;
Expand All @@ -12,6 +15,7 @@ impl ModuleDef for UtilModule {
fn declare(declare: &mut Declarations) -> Result<()> {
declare.declare(stringify!(TextDecoder))?;
declare.declare(stringify!(TextEncoder))?;
declare.declare(stringify!(format))?;
declare.declare_static(cstr!("default"))?;
Ok(())
}
Expand All @@ -25,6 +29,7 @@ impl ModuleDef for UtilModule {

default.set(stringify!(TextEncoder), encoder)?;
default.set(stringify!(TextDecoder), decoder)?;
default.set("format", Func::from(format))?;

Ok(())
})
Expand All @@ -39,3 +44,11 @@ impl From<UtilModule> for ModuleInfo<UtilModule> {
}
}
}

fn format<'js>(ctx: Ctx<'js>, args: Rest<Value<'js>>) -> Result<String> {
let mut result = String::with_capacity(64);
let newline_char = NEWLINE_LOOKUP[AWS_LAMBDA_MODE.load(Ordering::Relaxed) as usize];
format_values_internal(&mut result, &ctx, args, false, newline_char)?;

Ok(result)
}
16 changes: 16 additions & 0 deletions tests/unit/console.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import * as timers from "timers";
import { Console as NodeConsole } from "node:console";
import { Console } from "console";
import util from "util";

function log(...args: any[]) {
return (console as any).__formatPlain(...args);
}

it("should format strings correctly", () => {

expect(util.format('%s:%s', 'foo', "bar")).toEqual("foo:bar")
expect(util.format(1, 2, 3)).toEqual("1 2 3")
expect(util.format("%% %s")).toEqual("%% %s")
expect(util.format("%s:%s", "foo")).toEqual("foo:%s")
expect(util.format("Hello %%, %s! How are you, %s?", "Alice", "Bob")).toEqual("Hello %, Alice! How are you, Bob?")
expect(util.format("The %s %d %f.", "quick", "42", "3.14")).toEqual("The quick 42 3.14.")
expect(util.format("Unmatched placeholders: %s %x %% %q", "one", "two")).toEqual("Unmatched placeholders: one %x % %q two")
expect(util.format("Unmatched placeholders: %s", "one", "two","three")).toEqual("Unmatched placeholders: one two three")

// Should not throw any exceptions
console.log('%s:%s', 'foo', "bar")
})

it("should log module", () => {
let module = log(timers);

Expand Down

0 comments on commit 8043098

Please sign in to comment.