aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_utils
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tests/test_utils/hast.rs10
-rw-r--r--tests/test_utils/jsx_rewrite.rs261
-rw-r--r--tests/test_utils/micromark_swc_utils.rs134
-rw-r--r--tests/test_utils/mod.rs2
-rw-r--r--tests/test_utils/swc.rs145
-rw-r--r--tests/test_utils/swc_utils.rs96
-rw-r--r--tests/test_utils/to_document.rs65
-rw-r--r--tests/test_utils/to_hast.rs22
-rw-r--r--tests/test_utils/to_swc.rs78
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;