#[allow(clippy::wildcard_imports, reason = "many AST types used")]
use oxc_ast::ast::*;
const MAX_CONTEXT_LITERAL_CHARS: usize = 40;
const CUT_LITERAL_SUFFIX: &str = "...";
const SPAN_NAMED_CONSTRUCTOR: &str = "fn";
pub fn span_name(call: &CallExpression<'_>) -> Option<String> {
let Expression::CallExpression(constructor) = call.callee.without_parentheses() else {
return None;
};
let is_span_constructor = match constructor.callee.without_parentheses() {
Expression::Identifier(ident) => ident.name == SPAN_NAMED_CONSTRUCTOR,
Expression::StaticMemberExpression(member) => {
member.property.name == SPAN_NAMED_CONSTRUCTOR
}
_ => false,
};
if !is_span_constructor {
return None;
}
constructor
.arguments
.first()
.and_then(argument_literal)
.map(|literal| cut_literal(&literal))
}
pub fn string_keyed_call_name(call: &CallExpression<'_>) -> Option<String> {
if !call.arguments.iter().skip(1).any(can_hold_named_function) {
return None;
}
let literal = call.arguments.first().and_then(argument_literal)?;
let callee = callee_label(&call.callee)?;
let quoted = cut_literal(&literal)
.replace('\\', "\\\\")
.replace('"', "\\\"");
Some(format!("{callee}(\"{quoted}\")"))
}
const fn can_hold_named_function(argument: &Argument<'_>) -> bool {
matches!(
argument,
Argument::FunctionExpression(_)
| Argument::ArrowFunctionExpression(_)
| Argument::CallExpression(_)
)
}
pub fn returned_generator_call<'a, 'b>(expr: &'b Expression<'a>) -> Option<&'b CallExpression<'a>> {
match expr.without_parentheses() {
Expression::TSAsExpression(inner) => returned_generator_call(&inner.expression),
Expression::TSSatisfiesExpression(inner) => returned_generator_call(&inner.expression),
Expression::CallExpression(call) => {
if has_generator_argument(call) {
return Some(call);
}
match call.callee.without_parentheses() {
Expression::StaticMemberExpression(member) if member.property.name == "pipe" => {
returned_generator_call(&member.object)
}
_ => None,
}
}
_ => None,
}
}
pub fn is_anonymous_generator(argument: &Argument<'_>) -> bool {
matches!(argument, Argument::FunctionExpression(func) if func.generator && func.id.is_none())
}
pub fn is_placeholder_name(name: &str) -> bool {
name.starts_with('<')
}
fn has_generator_argument(call: &CallExpression<'_>) -> bool {
call.arguments.iter().any(is_anonymous_generator)
}
fn argument_literal(argument: &Argument<'_>) -> Option<String> {
let raw = match argument {
Argument::StringLiteral(literal) => literal.value.as_str(),
Argument::TemplateLiteral(template) if template.expressions.is_empty() => template
.quasis
.first()
.and_then(|quasi| quasi.value.cooked.as_ref())
.map(|cooked| cooked.as_str())?,
_ => return None,
};
Some(
raw.chars()
.map(|c| if breaks_one_line_name(c) { ' ' } else { c })
.collect(),
)
}
fn breaks_one_line_name(c: char) -> bool {
c.is_control() || c == '|' || c == '`'
}
fn cut_literal(literal: &str) -> String {
if literal.chars().count() <= MAX_CONTEXT_LITERAL_CHARS {
return literal.to_owned();
}
let kept: String = literal.chars().take(MAX_CONTEXT_LITERAL_CHARS).collect();
format!("{}{CUT_LITERAL_SUFFIX}", kept.trim_end())
}
fn callee_label(callee: &Expression<'_>) -> Option<String> {
match callee.without_parentheses() {
Expression::Identifier(ident) => Some(ident.name.to_string()),
Expression::StaticMemberExpression(member) => {
Some(dotted_path(&member.object).map_or_else(
|| member.property.name.to_string(),
|object| format!("{object}.{}", member.property.name),
))
}
_ => None,
}
}
fn dotted_path(expr: &Expression<'_>) -> Option<String> {
match expr.without_parentheses() {
Expression::Identifier(ident) => Some(ident.name.to_string()),
Expression::StaticMemberExpression(member) => {
dotted_path(&member.object).map(|object| format!("{object}.{}", member.property.name))
}
_ => None,
}
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
use oxc_allocator::Allocator;
use oxc_parser::Parser;
use oxc_span::SourceType;
fn with_expression<T>(source: &str, read: impl FnOnce(&Expression<'_>) -> T) -> T {
let allocator = Allocator::default();
let parsed = Parser::new(&allocator, source, SourceType::ts()).parse();
let Some(Statement::ExpressionStatement(stmt)) = parsed.program.body.first() else {
panic!("expected one expression statement in {source}");
};
read(&stmt.expression)
}
fn call_name(source: &str, name: fn(&CallExpression<'_>) -> Option<String>) -> Option<String> {
with_expression(source, |expr| match expr {
Expression::CallExpression(call) => name(call),
_ => panic!("expected a call in {source}"),
})
}
#[test]
fn span_name_reads_the_effect_fn_literal() {
assert_eq!(
call_name("Effect.fn(\"Users.create\")(function* () {})", span_name),
Some("Users.create".to_owned())
);
assert_eq!(
call_name("fn(`Users.create`)(function* () {})", span_name),
Some("Users.create".to_owned())
);
}
#[test]
fn span_name_ignores_other_curried_calls() {
assert_eq!(
call_name("Effect.gen(\"x\")(function* () {})", span_name),
None
);
assert_eq!(
call_name("Effect.fn(name)(function* () {})", span_name),
None
);
assert_eq!(call_name("Effect.fn(function* () {})", span_name), None);
}
#[test]
fn string_keyed_call_name_uses_the_dotted_callee() {
assert_eq!(
call_name(
"Command.make(\"setup\", {}, () => 1)",
string_keyed_call_name
),
Some("Command.make(\"setup\")".to_owned())
);
assert_eq!(
call_name(
"api.group().handle(\"github\", () => 1)",
string_keyed_call_name
),
Some("handle(\"github\")".to_owned())
);
assert_eq!(
call_name("it(\"works\", () => 1)", string_keyed_call_name),
Some("it(\"works\")".to_owned())
);
}
#[test]
fn string_keyed_call_name_needs_a_literal_first_argument() {
assert_eq!(
call_name(
"Match.when({ type: \"a\" }, () => 1)",
string_keyed_call_name
),
None
);
assert_eq!(
call_name("on(`a${b}`, () => 1)", string_keyed_call_name),
None
);
assert_eq!(
call_name("f()(\"a\", () => 1)", string_keyed_call_name),
None
);
}
#[test]
fn long_literals_are_cut() {
let name = call_name(
"test(\"creates, updates, and deletes a machine in one region\", () => 1)",
string_keyed_call_name,
);
assert_eq!(
name,
Some("test(\"creates, updates, and deletes a machine...\")".to_owned())
);
}
#[test]
fn control_characters_become_spaces() {
assert_eq!(
call_name("it(`line one\nline two`, () => 1)", string_keyed_call_name),
Some("it(\"line one line two\")".to_owned())
);
}
#[test]
fn multi_byte_literals_are_cut_by_character() {
let title = "\u{e9}".repeat(50);
let name = call_name(&format!("it(\"{title}\", () => 1)"), string_keyed_call_name);
assert_eq!(name, Some(format!("it(\"{}...\")", "\u{e9}".repeat(40))));
}
#[test]
fn markdown_breaking_characters_become_spaces() {
assert_eq!(
call_name("it(\"a | b `c`\", () => 1)", string_keyed_call_name),
Some("it(\"a b c \")".to_owned())
);
}
#[test]
fn backslashes_in_literals_get_a_backslash() {
assert_eq!(
call_name(r#"it("a\\", () => 1)"#, string_keyed_call_name),
Some(r#"it("a\\")"#.to_owned())
);
}
#[test]
fn calls_without_a_function_argument_get_no_name() {
assert_eq!(call_name("t(\"key\", 1)", string_keyed_call_name), None);
}
#[test]
fn double_quotes_in_literals_get_a_backslash() {
assert_eq!(
call_name(r#"it('reads "x"', () => 1)"#, string_keyed_call_name),
Some(r#"it("reads \"x\"")"#.to_owned())
);
}
#[test]
fn returned_generator_call_looks_through_pipe_and_assertions() {
for source in [
"Effect.gen(function* () {})",
"(Effect.gen(function* () {}))",
"Effect.gen(function* () {}).pipe(Effect.withSpan(\"x\"))",
"Effect.gen(function* () {}) as Effect.Effect<void>",
] {
assert!(
with_expression(source, |expr| returned_generator_call(expr).is_some()),
"{source}"
);
}
}
#[test]
fn returned_generator_call_needs_an_anonymous_generator() {
for source in [
"items.map((item) => item)",
"Effect.gen(function* named() {})",
"run(function () {})",
"Effect.succeed(1).pipe(Effect.map((x) => x))",
] {
assert!(
with_expression(source, |expr| returned_generator_call(expr).is_none()),
"{source}"
);
}
}
}