diff options
Diffstat (limited to '')
-rw-r--r-- | tests/test_utils/hast.rs | 10 | ||||
-rw-r--r-- | tests/test_utils/jsx_rewrite.rs | 261 | ||||
-rw-r--r-- | tests/test_utils/micromark_swc_utils.rs | 134 | ||||
-rw-r--r-- | tests/test_utils/mod.rs | 2 | ||||
-rw-r--r-- | tests/test_utils/swc.rs | 145 | ||||
-rw-r--r-- | tests/test_utils/swc_utils.rs | 96 | ||||
-rw-r--r-- | tests/test_utils/to_document.rs | 65 | ||||
-rw-r--r-- | tests/test_utils/to_hast.rs | 22 | ||||
-rw-r--r-- | tests/test_utils/to_swc.rs | 78 |
9 files changed, 565 insertions, 248 deletions
diff --git a/tests/test_utils/hast.rs b/tests/test_utils/hast.rs index 1ad8789..48460ca 100644 --- a/tests/test_utils/hast.rs +++ b/tests/test_utils/hast.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -// ^-- fix later +// ^-- To do: fix later extern crate alloc; extern crate micromark; @@ -9,7 +9,7 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -pub use micromark::mdast::{AttributeContent, AttributeValue, MdxJsxAttribute}; +pub use micromark::mdast::{AttributeContent, AttributeValue, MdxJsxAttribute, Stop}; use micromark::unist::Position; /// Nodes. @@ -254,6 +254,9 @@ pub struct MdxExpression { pub value: String, /// Positional info. pub position: Option<Position>, + + // Custom data on where each slice of `value` came from. + pub stops: Vec<Stop>, } /// MDX: ESM. @@ -269,4 +272,7 @@ pub struct MdxjsEsm { pub value: String, /// Positional info. pub position: Option<Position>, + + // Custom data on where each slice of `value` came from. + pub stops: Vec<Stop>, } diff --git a/tests/test_utils/jsx_rewrite.rs b/tests/test_utils/jsx_rewrite.rs index b6ffad6..9dd2605 100644 --- a/tests/test_utils/jsx_rewrite.rs +++ b/tests/test_utils/jsx_rewrite.rs @@ -1,9 +1,13 @@ extern crate swc_common; extern crate swc_ecma_ast; -use crate::{ - micromark::{id_cont_ as id_cont, id_start_ as id_start}, - test_utils::to_swc::Program, +use crate::test_utils::{ + micromark_swc_utils::{position_to_string, span_to_position}, + swc_utils::{ + create_binary_expression, create_ident, create_ident_expression, create_member_expression, + }, + to_swc::Program, }; +use micromark::{id_cont_ as id_cont, id_start_ as id_start, unist::Position, Location}; use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith}; /// Configuration. @@ -21,10 +25,17 @@ pub struct Options { /// Rewrite JSX in an MDX file so that components can be passed in and provided. #[allow(dead_code)] -pub fn jsx_rewrite(mut program: Program, options: &Options) -> Program { +pub fn jsx_rewrite( + mut program: Program, + options: &Options, + location: Option<&Location>, +) -> Program { let mut state = State { scopes: vec![], + location, provider: options.provider_import_source.is_some(), + path: program.path.clone(), + development: options.development, create_provider_import: false, create_error_helper: false, }; @@ -44,7 +55,10 @@ pub fn jsx_rewrite(mut program: Program, options: &Options) -> Program { // If potentially missing components are used, add the helper used for // errors. if state.create_error_helper { - program.module.body.push(create_error_helper()); + program + .module + .body + .push(create_error_helper(state.development, state.path)); } program @@ -88,8 +102,7 @@ struct Info { /// ``` /// vec![("a".into(), false), ("a.b".into(), true)] /// ``` - // To do: add positional info later. - references: Vec<(String, bool)>, + references: Vec<(String, bool, Option<Position>)>, } /// Scope (block or function/global). @@ -103,9 +116,14 @@ struct Scope { /// Context. #[derive(Debug, Default, Clone)] -struct State { +struct State<'a> { + location: Option<&'a Location>, + /// Path to file. + path: Option<String>, /// List of current scopes. scopes: Vec<Scope>, + /// Whether the user is in development mode. + development: bool, /// Whether the user uses a provider. provider: bool, /// Whether a provider is referenced. @@ -117,7 +135,7 @@ struct State { create_error_helper: bool, } -impl State { +impl<'a> State<'a> { /// Open a new scope. fn enter(&mut self, info: Option<Info>) { self.scopes.push(Scope { @@ -432,8 +450,47 @@ impl State { // if (!a) _missingMdxReference("a", false); // if (!a.b) _missingMdxReference("a.b", true); // ``` - for (id, component) in info.references { + for (id, component, position) in info.references { self.create_error_helper = true; + + let mut args = vec![ + swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: id.clone().into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + }, + swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Bool( + swc_ecma_ast::Bool { + value: component, + span: swc_common::DUMMY_SP, + }, + ))), + }, + ]; + + // Add the source location if it exists and if `development` is on. + if let Some(position) = position.as_ref() { + if self.development { + args.push(swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: position_to_string(position).into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + }) + } + } + statements.push(swc_ecma_ast::Stmt::If(swc_ecma_ast::IfStmt { test: Box::new(swc_ecma_ast::Expr::Unary(swc_ecma_ast::UnaryExpr { op: swc_ecma_ast::UnaryOp::Bang, @@ -446,27 +503,7 @@ impl State { callee: swc_ecma_ast::Callee::Expr(Box::new(create_ident_expression( "_missingMdxReference", ))), - args: vec![ - swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: id.into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - }, - swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Bool( - swc_ecma_ast::Bool { - value: component, - span: swc_common::DUMMY_SP, - }, - ))), - }, - ], + args, type_args: None, span: swc_common::DUMMY_SP, })), @@ -545,6 +582,7 @@ impl State { None } } + /// Get the top-level scope’s info, mutably. fn current_top_level_info_mut(&mut self) -> Option<&mut Info> { if let Some(scope) = self.scopes.get_mut(1) { @@ -623,7 +661,7 @@ impl State { } } -impl VisitMut for State { +impl<'a> VisitMut for State<'a> { noop_visit_mut_type!(); /// Rewrite JSX identifiers. @@ -632,6 +670,7 @@ impl VisitMut for State { if let Some(info) = self.current_top_level_info() { // Rewrite only if we can rewrite. if is_props_receiving_fn(&info.name) || self.provider { + let position = span_to_position(&node.span, self.location); match &node.opening.name { // `<x.y>`, `<Foo.Bar>`, `<x.y.z>`. swc_ecma_ast::JSXElementName::JSXMemberExpr(d) => { @@ -656,7 +695,6 @@ impl VisitMut for State { if !in_scope { let info_mut = self.current_top_level_info_mut().unwrap(); - // To do: add positional info. let mut index = 1; while index <= ids.len() { let full_id = ids[0..index].join("."); @@ -668,7 +706,9 @@ impl VisitMut for State { reference.1 = true; } } else { - info_mut.references.push((full_id, component)) + info_mut + .references + .push((full_id, component, position.clone())) } index += 1; } @@ -749,7 +789,7 @@ impl VisitMut for State { { reference.1 = true; } else { - info_mut.references.push((id.clone(), true)) + info_mut.references.push((id.clone(), true, position)) } } @@ -948,8 +988,8 @@ fn create_import_provider(source: &str) -> swc_ecma_ast::ModuleItem { /// throw new Error("Expected " + (component ? "component" : "object") + " `" + id + "` to be defined: you likely forgot to import, pass, or provide it."); /// } /// ``` -fn create_error_helper() -> swc_ecma_ast::ModuleItem { - let parameters = vec![ +fn create_error_helper(development: bool, path: Option<String>) -> swc_ecma_ast::ModuleItem { + let mut parameters = vec![ swc_ecma_ast::Param { pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { id: create_ident("id"), @@ -968,7 +1008,19 @@ fn create_error_helper() -> swc_ecma_ast::ModuleItem { }, ]; - let message = vec![ + // Accept a source location (which might be undefiend). + if development { + parameters.push(swc_ecma_ast::Param { + pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: create_ident("place"), + type_ann: None, + }), + decorators: vec![], + span: swc_common::DUMMY_SP, + }) + } + + let mut message = vec![ swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { value: "Expected ".into(), span: swc_common::DUMMY_SP, @@ -1009,8 +1061,43 @@ fn create_error_helper() -> swc_ecma_ast::ModuleItem { })), ]; - // To do: in development, add `place` param, and use the positional info. - // Also, then, add file path. + // `place ? "\nIt’s referenced in your code at `" + place+ "`" : ""` + if development { + message.push(swc_ecma_ast::Expr::Paren(swc_ecma_ast::ParenExpr { + expr: Box::new(swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { + test: Box::new(create_ident_expression("place")), + cons: Box::new(create_binary_expression( + vec![ + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { + value: "\nIt’s referenced in your code at `".into(), + span: swc_common::DUMMY_SP, + raw: None, + })), + create_ident_expression("place"), + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { + value: if let Some(path) = path { + format!("` in `{}`", path).into() + } else { + "`".into() + }, + span: swc_common::DUMMY_SP, + raw: None, + })), + ], + swc_ecma_ast::BinaryOp::Add, + )), + alt: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: "".into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + span: swc_common::DUMMY_SP, + })), + span: swc_common::DUMMY_SP, + })) + } swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Fn( swc_ecma_ast::FnDecl { @@ -1047,100 +1134,6 @@ fn create_error_helper() -> swc_ecma_ast::ModuleItem { ))) } -/// Generate a binary expression. -/// -/// ```js -/// a + b + c -/// a || b -/// ``` -fn create_binary_expression( - mut exprs: Vec<swc_ecma_ast::Expr>, - op: swc_ecma_ast::BinaryOp, -) -> swc_ecma_ast::Expr { - exprs.reverse(); - - let mut left = None; - - while let Some(right_expr) = exprs.pop() { - left = Some(if let Some(left_expr) = left { - swc_ecma_ast::Expr::Bin(swc_ecma_ast::BinExpr { - left: Box::new(left_expr), - right: Box::new(right_expr), - op, - span: swc_common::DUMMY_SP, - }) - } else { - right_expr - }); - } - - left.expect("expected one or more expressions") -} - -/// Generate a member expression. -/// -/// ```js -/// a.b -/// a -/// ``` -fn create_member_expression(name: &str) -> swc_ecma_ast::Expr { - let bytes = name.as_bytes(); - let mut index = 0; - let mut start = 0; - let mut parts = vec![]; - - while index < bytes.len() { - if bytes[index] == b'.' { - parts.push(&name[start..index]); - start = index + 1; - } - - index += 1; - } - - if parts.len() > 1 { - let mut member = swc_ecma_ast::MemberExpr { - obj: Box::new(create_ident_expression(parts[0])), - prop: swc_ecma_ast::MemberProp::Ident(create_ident(parts[1])), - span: swc_common::DUMMY_SP, - }; - let mut index = 2; - while index < parts.len() { - member = swc_ecma_ast::MemberExpr { - obj: Box::new(swc_ecma_ast::Expr::Member(member)), - prop: swc_ecma_ast::MemberProp::Ident(create_ident(parts[1])), - span: swc_common::DUMMY_SP, - }; - index += 1; - } - swc_ecma_ast::Expr::Member(member) - } else { - create_ident_expression(name) - } -} - -/// Generate an ident expression. -/// -/// ```js -/// a -/// ``` -fn create_ident_expression(sym: &str) -> swc_ecma_ast::Expr { - swc_ecma_ast::Expr::Ident(create_ident(sym)) -} - -/// Generate an ident. -/// -/// ```js -/// a -/// ``` -fn create_ident(sym: &str) -> swc_ecma_ast::Ident { - swc_ecma_ast::Ident { - sym: sym.into(), - optional: false, - span: swc_common::DUMMY_SP, - } -} - /// Check if this function is a props receiving component: it’s one of ours. fn is_props_receiving_fn(name: &Option<String>) -> bool { if let Some(name) = name { diff --git a/tests/test_utils/micromark_swc_utils.rs b/tests/test_utils/micromark_swc_utils.rs new file mode 100644 index 0000000..13678d5 --- /dev/null +++ b/tests/test_utils/micromark_swc_utils.rs @@ -0,0 +1,134 @@ +extern crate swc_common; +use micromark::{ + mdast::Stop, + unist::{Point, Position}, + Location, +}; +use swc_common::{BytePos, Span, SyntaxContext, DUMMY_SP}; +use swc_ecma_visit::{noop_visit_mut_type, VisitMut}; + +/// Turn a unist position, into an SWC span, of two byte positions. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn position_to_span(position: Option<&Position>) -> Span { + position.map_or(DUMMY_SP, |d| Span { + lo: point_to_bytepos(&d.start), + hi: point_to_bytepos(&d.end), + ctxt: SyntaxContext::empty(), + }) +} + +/// Turn an SWC span, of two byte positions, into a unist position. +/// +/// This assumes the span comes from a fixed tree, or is a dummy. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn span_to_position(span: &Span, location: Option<&Location>) -> Option<Position> { + let lo = span.lo.0 as usize; + let hi = span.hi.0 as usize; + + if lo > 0 && hi > 0 { + if let Some(location) = location { + if let Some(start) = location.to_point(lo - 1) { + if let Some(end) = location.to_point(hi - 1) { + return Some(Position { start, end }); + } + } + } + } + + None +} + +/// Turn a unist point into an SWC byte position. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn point_to_bytepos(point: &Point) -> BytePos { + BytePos(point.offset as u32 + 1) +} + +/// Turn an SWC byte position into a unist point. +/// +/// This assumes the byte position comes from a fixed tree, or is a dummy. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn bytepos_to_point(bytepos: &BytePos, location: Option<&Location>) -> Option<Point> { + let pos = bytepos.0 as usize; + + if pos > 0 { + if let Some(location) = location { + return location.to_point(pos - 1); + } + } + + None +} + +/// Prefix an error message with an optional point. +pub fn prefix_error_with_point(reason: String, point: Option<&Point>) -> String { + if let Some(point) = point { + format!("{}: {}", point_to_string(point), reason) + } else { + reason + } +} + +/// Serialize a unist position for humans. +pub fn position_to_string(position: &Position) -> String { + format!( + "{}-{}", + point_to_string(&position.start), + point_to_string(&position.end) + ) +} + +/// Serialize a unist point for humans. +pub fn point_to_string(point: &Point) -> String { + format!("{}:{}", point.line, point.column) +} + +/// Visitor to fix SWC byte positions. +/// +/// This assumes the byte position comes from an **unfixed** tree. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +#[derive(Debug, Default, Clone)] +pub struct RewriteContext<'a> { + pub prefix_len: usize, + pub stops: &'a [Stop], + pub location: Option<&'a Location>, +} + +impl<'a> VisitMut for RewriteContext<'a> { + noop_visit_mut_type!(); + + // Rewrite spans. + fn visit_mut_span(&mut self, span: &mut Span) { + let mut result = DUMMY_SP; + let lo_rel = span.lo.0 as usize; + let hi_rel = span.hi.0 as usize; + + if lo_rel > self.prefix_len && hi_rel > self.prefix_len { + if let Some(lo_abs) = + Location::relative_to_absolute(self.stops, lo_rel - 1 - self.prefix_len) + { + if let Some(hi_abs) = + Location::relative_to_absolute(self.stops, hi_rel - 1 - self.prefix_len) + { + result = Span { + lo: BytePos(lo_abs as u32 + 1), + hi: BytePos(hi_abs as u32 + 1), + ctxt: SyntaxContext::empty(), + }; + } + } + } + + *span = result; + } +} diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs index 339992c..99ded2f 100644 --- a/tests/test_utils/mod.rs +++ b/tests/test_utils/mod.rs @@ -1,6 +1,8 @@ pub mod hast; pub mod jsx_rewrite; +pub mod micromark_swc_utils; pub mod swc; +pub mod swc_utils; pub mod to_document; pub mod to_hast; pub mod to_swc; diff --git a/tests/test_utils/swc.rs b/tests/test_utils/swc.rs index fb91a3b..78859b6 100644 --- a/tests/test_utils/swc.rs +++ b/tests/test_utils/swc.rs @@ -2,7 +2,10 @@ extern crate micromark; extern crate swc_common; extern crate swc_ecma_ast; extern crate swc_ecma_parser; -use micromark::{MdxExpressionKind, MdxSignal}; +use crate::test_utils::micromark_swc_utils::{ + bytepos_to_point, prefix_error_with_point, RewriteContext, +}; +use micromark::{mdast::Stop, unist::Point, Location, MdxExpressionKind, MdxSignal}; use swc_common::{ source_map::Pos, sync::Lrc, BytePos, FileName, FilePathMapping, SourceFile, SourceMap, Spanned, }; @@ -11,10 +14,7 @@ use swc_ecma_codegen::{text_writer::JsWriter, Emitter}; use swc_ecma_parser::{ error::Error as SwcError, parse_file_as_expr, parse_file_as_module, EsConfig, Syntax, }; - -// To do: -// Use lexer in the future: -// <https://docs.rs/swc_ecma_parser/0.99.1/swc_ecma_parser/lexer/index.html> +use swc_ecma_visit::VisitMutWith; /// Lex ESM in MDX with SWC. #[allow(dead_code)] @@ -24,40 +24,42 @@ pub fn parse_esm(value: &str) -> MdxSignal { let result = parse_file_as_module(&file, syntax, version, None, &mut errors); match result { - Err(error) => swc_error_to_signal(&error, value.len(), 0, "esm"), + Err(error) => swc_error_to_signal(&error, "esm", value.len(), 0), Ok(tree) => { if errors.is_empty() { check_esm_ast(&tree) } else { - if errors.len() > 1 { - println!("parse_esm: todo: multiple errors? {:?}", errors); - } - swc_error_to_signal(&errors[0], value.len(), 0, "esm") + swc_error_to_signal(&errors[0], "esm", value.len(), 0) } } } } /// Parse ESM in MDX with SWC. -/// To do: figure out how to fix positional info. /// See `drop_span` in `swc_ecma_utils` for inspiration? #[allow(dead_code)] -pub fn parse_esm_to_tree(value: &str) -> Result<swc_ecma_ast::Module, String> { +pub fn parse_esm_to_tree( + value: &str, + stops: &[Stop], + location: Option<&Location>, +) -> Result<swc_ecma_ast::Module, String> { let (file, syntax, version) = create_config(value.to_string()); let mut errors = vec![]; - let result = parse_file_as_module(&file, syntax, version, None, &mut errors); + let mut rewrite_context = RewriteContext { + stops, + location, + prefix_len: 0, + }; match result { - Err(error) => Err(swc_error_to_string(&error)), - Ok(module) => { + Err(error) => Err(swc_error_to_error(&error, "esm", &rewrite_context)), + Ok(mut module) => { if errors.is_empty() { + module.visit_mut_with(&mut rewrite_context); Ok(module) } else { - if errors.len() > 1 { - println!("parse_esm_to_tree: todo: multiple errors? {:?}", errors); - } - Err(swc_error_to_string(&errors[0])) + Err(swc_error_to_error(&errors[0], "esm", &rewrite_context)) } } } @@ -87,33 +89,31 @@ pub fn parse_expression(value: &str, kind: &MdxExpressionKind) -> MdxSignal { let result = parse_file_as_expr(&file, syntax, version, None, &mut errors); match result { - Err(error) => swc_error_to_signal(&error, value.len(), prefix.len(), "expression"), + Err(error) => swc_error_to_signal(&error, "expression", value.len(), prefix.len()), Ok(tree) => { if errors.is_empty() { - let place = fix_swc_position(tree.span().hi.to_usize(), prefix.len()); + let expression_end = fix_swc_position(tree.span().hi.to_usize(), prefix.len()); let result = check_expression_ast(&tree, kind); if matches!(result, MdxSignal::Ok) { - whitespace_and_comments(place, value) + whitespace_and_comments(expression_end, value) } else { result } } else { - if errors.len() > 1 { - unreachable!("parse_expression: todo: multiple errors? {:?}", errors); - } - swc_error_to_signal(&errors[0], value.len(), prefix.len(), "expression") + swc_error_to_signal(&errors[0], "expression", value.len(), prefix.len()) } } } } /// Parse ESM in MDX with SWC. -/// To do: figure out how to fix positional info. /// See `drop_span` in `swc_ecma_utils` for inspiration? #[allow(dead_code)] pub fn parse_expression_to_tree( value: &str, kind: &MdxExpressionKind, + stops: &[Stop], + location: Option<&Location>, ) -> Result<Box<swc_ecma_ast::Expr>, String> { // For attribute expression, a spread is needed, for which we have to prefix // and suffix the input. @@ -127,11 +127,21 @@ pub fn parse_expression_to_tree( let (file, syntax, version) = create_config(format!("{}{}{}", prefix, value, suffix)); let mut errors = vec![]; let result = parse_file_as_expr(&file, syntax, version, None, &mut errors); + let mut rewrite_context = RewriteContext { + stops, + location, + prefix_len: prefix.len(), + }; match result { - Err(error) => Err(swc_error_to_string(&error)), - Ok(expr) => { + Err(error) => Err(swc_error_to_error(&error, "expression", &rewrite_context)), + Ok(mut expr) => { if errors.is_empty() { + // Fix positions. + expr.visit_mut_with(&mut rewrite_context); + + let expr_bytepos = expr.span().lo; + if matches!(kind, MdxExpressionKind::AttributeExpression) { let mut obj = None; @@ -143,27 +153,37 @@ pub fn parse_expression_to_tree( if let Some(mut obj) = obj { if obj.props.len() > 1 { - Err("Unexpected extra content in spread: only a single spread is supported".into()) + Err(create_error_message( + "Unexpected extra content in spread: only a single spread is supported", + "expression", + bytepos_to_point(&obj.span.lo, location).as_ref() + )) } else if let Some(swc_ecma_ast::PropOrSpread::Spread(d)) = obj.props.pop() { Ok(d.expr) } else { - Err("Unexpected prop in spread: only a spread is supported".into()) + Err(create_error_message( + "Unexpected prop in spread: only a spread is supported", + "expression", + bytepos_to_point(&obj.span.lo, location).as_ref(), + )) } } else { - Err("Expected an object spread (`{...spread}`)".into()) + Err(create_error_message( + "Expected an object spread (`{...spread}`)", + "expression", + bytepos_to_point(&expr_bytepos, location).as_ref(), + )) } } else { Ok(expr) } } else { - if errors.len() > 1 { - println!( - "parse_expression_to_tree: todo: multiple errors? {:?}", - errors - ); - } - Err(swc_error_to_string(&errors[0])) + Err(swc_error_to_error( + &errors[0], + "expression", + &rewrite_context, + )) } } } @@ -203,10 +223,10 @@ fn check_esm_ast(tree: &Module) -> MdxSignal { let node = &tree.body[index]; if !node.is_module_decl() { - let place = fix_swc_position(node.span().hi.to_usize(), 0); + let relative = fix_swc_position(node.span().lo.to_usize(), 0); return MdxSignal::Error( "Unexpected statement in code: only import/exports are supported".to_string(), - place, + relative, ); } @@ -248,24 +268,47 @@ fn check_expression_ast(tree: &Expr, kind: &MdxExpressionKind) -> MdxSignal { /// * Else, yields `MdxSignal::Error`. fn swc_error_to_signal( error: &SwcError, + name: &str, value_len: usize, prefix_len: usize, - name: &str, ) -> MdxSignal { - let place = fix_swc_position(error.span().hi.to_usize(), prefix_len); - let message = format!( - "Could not parse {} with swc: {}", - name, - swc_error_to_string(error) - ); + let reason = create_error_reason(&swc_error_to_string(error), name); + let error_end = fix_swc_position(error.span().hi.to_usize(), prefix_len); - if place >= value_len { - MdxSignal::Eof(message) + if error_end >= value_len { + MdxSignal::Eof(reason) } else { - MdxSignal::Error(message, place) + MdxSignal::Error( + reason, + fix_swc_position(error.span().lo.to_usize(), prefix_len), + ) } } +fn swc_error_to_error(error: &SwcError, name: &str, context: &RewriteContext) -> String { + create_error_message( + &swc_error_to_string(error), + name, + context + .location + .and_then(|location| { + location.relative_to_point( + context.stops, + fix_swc_position(error.span().lo.to_usize(), context.prefix_len), + ) + }) + .as_ref(), + ) +} + +fn create_error_message(reason: &str, name: &str, point: Option<&Point>) -> String { + prefix_error_with_point(create_error_reason(name, reason), point) +} + +fn create_error_reason(reason: &str, name: &str) -> String { + format!("Could not parse {} with swc: {}", name, reason) +} + /// Turn an SWC error into a string. fn swc_error_to_string(error: &SwcError) -> String { error.kind().msg().into() diff --git a/tests/test_utils/swc_utils.rs b/tests/test_utils/swc_utils.rs new file mode 100644 index 0000000..1e1a526 --- /dev/null +++ b/tests/test_utils/swc_utils.rs @@ -0,0 +1,96 @@ +extern crate swc_common; +extern crate swc_ecma_ast; + +use swc_common::DUMMY_SP; +use swc_ecma_ast::{BinExpr, BinaryOp, Expr, Ident, MemberExpr, MemberProp}; + +/// Generate an ident. +/// +/// ```js +/// a +/// ``` +pub fn create_ident(sym: &str) -> Ident { + Ident { + sym: sym.into(), + optional: false, + span: DUMMY_SP, + } +} + +/// Generate an ident expression. +/// +/// ```js +/// a +/// ``` +pub fn create_ident_expression(sym: &str) -> Expr { + Expr::Ident(create_ident(sym)) +} + +/// Generate a binary expression. +/// +/// ```js +/// a + b + c +/// a || b +/// ``` +pub fn create_binary_expression(mut exprs: Vec<Expr>, op: BinaryOp) -> Expr { + exprs.reverse(); + + let mut left = None; + + while let Some(right_expr) = exprs.pop() { + left = Some(if let Some(left_expr) = left { + Expr::Bin(BinExpr { + left: Box::new(left_expr), + right: Box::new(right_expr), + op, + span: swc_common::DUMMY_SP, + }) + } else { + right_expr + }); + } + + left.expect("expected one or more expressions") +} + +/// Generate a member expression. +/// +/// ```js +/// a.b +/// a +/// ``` +pub fn create_member_expression(name: &str) -> Expr { + let bytes = name.as_bytes(); + let mut index = 0; + let mut start = 0; + let mut parts = vec![]; + + while index < bytes.len() { + if bytes[index] == b'.' { + parts.push(&name[start..index]); + start = index + 1; + } + + index += 1; + } + + if parts.len() > 1 { + let mut member = MemberExpr { + obj: Box::new(create_ident_expression(parts[0])), + prop: MemberProp::Ident(create_ident(parts[1])), + span: swc_common::DUMMY_SP, + }; + let mut index = 2; + while index < parts.len() { + member = MemberExpr { + obj: Box::new(Expr::Member(member)), + prop: MemberProp::Ident(create_ident(parts[1])), + span: swc_common::DUMMY_SP, + }; + index += 1; + } + Expr::Member(member) + } else { + create_ident_expression(name) + } +} diff --git a/tests/test_utils/to_document.rs b/tests/test_utils/to_document.rs index ded028a..938df1b 100644 --- a/tests/test_utils/to_document.rs +++ b/tests/test_utils/to_document.rs @@ -1,6 +1,12 @@ -extern crate swc_common; extern crate swc_ecma_ast; -use crate::test_utils::to_swc::Program; +use crate::test_utils::{ + micromark_swc_utils::{bytepos_to_point, prefix_error_with_point, span_to_position}, + to_swc::Program, +}; +use micromark::{ + unist::{Point, Position}, + Location, +}; /// JSX runtimes. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -60,7 +66,11 @@ impl Default for Options { } #[allow(dead_code)] -pub fn to_document(mut program: Program, options: &Options) -> Result<Program, String> { +pub fn to_document( + mut program: Program, + options: &Options, + location: Option<&Location>, +) -> Result<Program, String> { // New body children. let mut replacements = vec![]; @@ -158,8 +168,8 @@ pub fn to_document(mut program: Program, options: &Options) -> Result<Program, S // is. let mut input = program.module.body.split_off(0); input.reverse(); - // To do: place position in this. let mut layout = false; + let mut layout_position = None; let content = true; while let Some(module_item) = input.pop() { @@ -174,13 +184,15 @@ pub fn to_document(mut program: Program, options: &Options) -> Result<Program, S swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultDecl( decl, )) => { - // To do: use positional info. if layout { - return Err("Cannot specify multiple layouts".into()); + return Err(create_double_layout_message( + bytepos_to_point(&decl.span.lo, location).as_ref(), + layout_position.as_ref(), + )); } - // To do: set positional info. layout = true; + layout_position = span_to_position(&decl.span, location); match decl.decl { swc_ecma_ast::DefaultDecl::Class(cls) => { replacements.push(create_layout_decl(swc_ecma_ast::Expr::Class(cls))) @@ -190,22 +202,26 @@ pub fn to_document(mut program: Program, options: &Options) -> Result<Program, S } swc_ecma_ast::DefaultDecl::TsInterfaceDecl(_) => { return Err( - "Cannot use TypeScript interface declarations as default export in MDX files. The default export is reserved for a layout, which must be a component" - .into(), - ) + prefix_error_with_point( + "Cannot use TypeScript interface declarations as default export in MDX files. The default export is reserved for a layout, which must be a component".into(), + bytepos_to_point(&decl.span.lo, location).as_ref() + ) + ); } } } swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultExpr( expr, )) => { - // To do: use positional info. if layout { - return Err("Cannot specify multiple layouts".into()); + return Err(create_double_layout_message( + bytepos_to_point(&expr.span.lo, location).as_ref(), + layout_position.as_ref(), + )); } - // To do: set positional info. layout = true; + layout_position = span_to_position(&expr.span, location); replacements.push(create_layout_decl(*expr.expr)); } // ```js @@ -239,12 +255,14 @@ pub fn to_document(mut program: Program, options: &Options) -> Result<Program, S // Looks like some TC39 proposal. Ignore for now // and only do things if this is an ID. if let swc_ecma_ast::ModuleExportName::Ident(ident) = &named.orig { - // To do: use positional info. if layout { - return Err("Cannot specify multiple layouts".into()); + return Err(create_double_layout_message( + bytepos_to_point(&ident.span.lo, location).as_ref(), + layout_position.as_ref(), + )); } - // To do: set positional info. layout = true; + layout_position = span_to_position(&ident.span, location); take = true; id = Some(ident.clone()); } @@ -623,3 +641,18 @@ fn create_layout_decl(expr: swc_ecma_ast::Expr) -> swc_ecma_ast::ModuleItem { }, )))) } + +/// Create an error message about multiple layouts. +fn create_double_layout_message(at: Option<&Point>, previous: Option<&Position>) -> String { + prefix_error_with_point( + format!( + "Cannot specify multiple layouts{}", + if let Some(previous) = previous { + format!(" (previous: {:?})", previous) + } else { + "".into() + } + ), + at, + ) +} diff --git a/tests/test_utils/to_hast.rs b/tests/test_utils/to_hast.rs index 0716daa..4907e23 100644 --- a/tests/test_utils/to_hast.rs +++ b/tests/test_utils/to_hast.rs @@ -827,10 +827,23 @@ fn transform_math(_state: &mut State, _node: &mdast::Node, math: &mdast::Math) - /// [`MdxFlowExpression`][mdast::MdxFlowExpression],[`MdxTextExpression`][mdast::MdxTextExpression]. fn transform_mdx_expression(_state: &mut State, node: &mdast::Node) -> Result { - Result::Node(hast::Node::MdxExpression(hast::MdxExpression { - value: node.to_string(), - position: node.position().cloned(), - })) + match node { + mdast::Node::MdxFlowExpression(node) => { + Result::Node(hast::Node::MdxExpression(hast::MdxExpression { + value: node.value.clone(), + position: node.position.clone(), + stops: node.stops.clone(), + })) + } + mdast::Node::MdxTextExpression(node) => { + Result::Node(hast::Node::MdxExpression(hast::MdxExpression { + value: node.value.clone(), + position: node.position.clone(), + stops: node.stops.clone(), + })) + } + _ => unreachable!("expected expression"), + } } /// [`MdxJsxFlowElement`][mdast::MdxJsxFlowElement],[`MdxJsxTextElement`][mdast::MdxJsxTextElement]. @@ -858,6 +871,7 @@ fn transform_mdxjs_esm( Result::Node(hast::Node::MdxjsEsm(hast::MdxjsEsm { value: mdxjs_esm.value.clone(), position: mdxjs_esm.position.clone(), + stops: mdxjs_esm.stops.clone(), })) } diff --git a/tests/test_utils/to_swc.rs b/tests/test_utils/to_swc.rs index 6c7312b..02de514 100644 --- a/tests/test_utils/to_swc.rs +++ b/tests/test_utils/to_swc.rs @@ -1,13 +1,18 @@ extern crate swc_common; extern crate swc_ecma_ast; -use crate::test_utils::hast; -use crate::test_utils::swc::{parse_esm_to_tree, parse_expression_to_tree}; +use crate::test_utils::{ + hast, + micromark_swc_utils::position_to_span, + swc::{parse_esm_to_tree, parse_expression_to_tree}, + swc_utils::create_ident, +}; use core::str; -use micromark::{unist::Position, MdxExpressionKind}; +use micromark::{Location, MdxExpressionKind}; /// Result. #[derive(Debug, PartialEq, Eq)] pub struct Program { + pub path: Option<String>, /// JS AST. pub module: swc_ecma_ast::Module, /// Comments relating to AST. @@ -22,7 +27,7 @@ enum Space { } #[derive(Debug)] -struct Context { +struct Context<'a> { /// Whether we’re in HTML or SVG. /// /// Not used yet, likely useful in the future. @@ -31,22 +36,22 @@ struct Context { comments: Vec<swc_common::comments::Comment>, /// Declarations and stuff. esm: Vec<swc_ecma_ast::ModuleItem>, -} - -impl Context { - /// Create a new context. - fn new() -> Context { - Context { - space: Space::Html, - comments: vec![], - esm: vec![], - } - } + /// Optional way to turn relative positions into points. + location: Option<&'a Location>, } #[allow(dead_code)] -pub fn to_swc(tree: &hast::Node) -> Result<Program, String> { - let mut context = Context::new(); +pub fn to_swc( + tree: &hast::Node, + path: Option<String>, + location: Option<&Location>, +) -> Result<Program, String> { + let mut context = Context { + space: Space::Html, + comments: vec![], + esm: vec![], + location, + }; let expr = match one(&mut context, tree)? { Some(swc_ecma_ast::JSXElementChild::JSXFragment(x)) => { Some(swc_ecma_ast::Expr::JSXFragment(x)) @@ -81,6 +86,7 @@ pub fn to_swc(tree: &hast::Node) -> Result<Program, String> { } Ok(Program { + path, module, comments: context.comments, }) @@ -260,12 +266,14 @@ fn transform_mdx_jsx_element( }, ))) } - Some(hast::AttributeValue::Expression(value)) => { + Some(hast::AttributeValue::Expression(value, stops)) => { Some(swc_ecma_ast::JSXAttrValue::JSXExprContainer( swc_ecma_ast::JSXExprContainer { expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree( value, &MdxExpressionKind::AttributeValueExpression, + stops, + context.location, )?), span: swc_common::DUMMY_SP, }, @@ -280,9 +288,13 @@ fn transform_mdx_jsx_element( value, }) } - hast::AttributeContent::Expression(value) => { - let expr = - parse_expression_to_tree(value, &MdxExpressionKind::AttributeExpression)?; + hast::AttributeContent::Expression(value, stops) => { + let expr = parse_expression_to_tree( + value, + &MdxExpressionKind::AttributeExpression, + stops, + context.location, + )?; swc_ecma_ast::JSXAttrOrSpread::SpreadElement(swc_ecma_ast::SpreadElement { dot3_token: swc_common::DUMMY_SP, expr, @@ -303,7 +315,7 @@ fn transform_mdx_jsx_element( /// [`MdxExpression`][hast::MdxExpression]. fn transform_mdx_expression( - _context: &mut Context, + context: &mut Context, node: &hast::Node, expression: &hast::MdxExpression, ) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { @@ -312,6 +324,8 @@ fn transform_mdx_expression( expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree( &expression.value, &MdxExpressionKind::Expression, + &expression.stops, + context.location, )?), span: position_to_span(node.position()), }, @@ -324,7 +338,7 @@ fn transform_mdxjs_esm( _node: &hast::Node, esm: &hast::MdxjsEsm, ) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { - let mut module = parse_esm_to_tree(&esm.value)?; + let mut module = parse_esm_to_tree(&esm.value, &esm.stops, context.location)?; let mut index = 0; // To do: check that identifiers are not duplicated across esm blocks. @@ -455,15 +469,6 @@ fn create_fragment( } } -/// Create an ident. -fn create_ident(sym: &str) -> swc_ecma_ast::Ident { - swc_ecma_ast::Ident { - sym: sym.into(), - optional: false, - span: swc_common::DUMMY_SP, - } -} - /// Create a JSX element name. fn create_jsx_name(name: &str) -> swc_ecma_ast::JSXElementName { match parse_jsx_name(name) { @@ -515,15 +520,6 @@ fn create_jsx_attr_name(name: &str) -> swc_ecma_ast::JSXAttrName { } } -/// Turn a unist positions into an SWC span. -fn position_to_span(position: Option<&Position>) -> swc_common::Span { - position.map_or(swc_common::DUMMY_SP, |d| swc_common::Span { - lo: swc_common::BytePos(d.start.offset as u32), - hi: swc_common::BytePos(d.end.offset as u32), - ctxt: swc_common::SyntaxContext::empty(), - }) -} - fn inter_element_whitespace(value: &str) -> bool { let bytes = value.as_bytes(); let mut index = 0; |