Skip to content

Commit 8bb526f

Browse files
authored
fix(parser): correct Astro shorthand attribute spans (#7)
1 parent 287b923 commit 8bb526f

2 files changed

Lines changed: 81 additions & 3 deletions

File tree

crates/oxc_parser/src/astro/jsx.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -602,27 +602,29 @@ impl<'a, C: ParserConfig> ParserImpl<'a, C> {
602602
/// Try to parse Astro shorthand attribute `{prop}` -> `prop={prop}`
603603
fn try_parse_astro_shorthand_attribute(&mut self) -> Option<Box<'a, JSXAttribute<'a>>> {
604604
let checkpoint = self.checkpoint();
605+
let attr_span = self.start_span();
605606
self.bump_any(); // bump `{`
606607

607608
if self.at(Kind::Ident) || self.cur_kind().is_any_keyword() {
608609
let ident_span = self.start_span();
609610
let name = self.cur_src();
610611
self.bump_any();
612+
let ident_span = self.end_span(ident_span);
611613

612614
if self.at(Kind::RCurly) {
613615
self.bump_any(); // bump `}`
614-
let ident_span = self.end_span(ident_span);
616+
let attr_span = self.end_span(attr_span);
615617
let name = Atom::from(name);
616618
let identifier = self.ast.jsx_identifier(ident_span, name);
617619
let attr_name = JSXAttributeName::Identifier(self.alloc(identifier));
618620

619621
let ident_ref = self.ast.identifier_reference(ident_span, name);
620622
let expr = Expression::Identifier(self.alloc(ident_ref));
621623
let expr_container =
622-
self.ast.alloc_jsx_expression_container(ident_span, JSXExpression::from(expr));
624+
self.ast.alloc_jsx_expression_container(attr_span, JSXExpression::from(expr));
623625
let value = JSXAttributeValue::ExpressionContainer(expr_container);
624626

625-
return Some(self.ast.alloc_jsx_attribute(ident_span, attr_name, Some(value)));
627+
return Some(self.ast.alloc_jsx_attribute(attr_span, attr_name, Some(value)));
626628
}
627629
}
628630

crates/oxc_parser/src/astro/mod.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,82 @@ const name = "World";
12681268
}
12691269
}
12701270

1271+
#[test]
1272+
fn parse_astro_attribute_shorthand_spans() {
1273+
use oxc_ast::ast::JSXAttributeValue;
1274+
1275+
let allocator = Allocator::default();
1276+
let source_type = SourceType::astro();
1277+
let source = r"<Component {prop} />";
1278+
let ret = Parser::new(&allocator, source, source_type).parse_astro();
1279+
assert!(!ret.panicked, "parser panicked: {:?}", ret.errors);
1280+
assert!(ret.errors.is_empty(), "errors: {:?}", ret.errors);
1281+
1282+
let JSXChild::Element(element) = &ret.root.body[0] else {
1283+
panic!("Expected JSXChild::Element");
1284+
};
1285+
let JSXAttributeItem::Attribute(attr) = &element.opening_element.attributes[0] else {
1286+
panic!("Expected Attribute, got SpreadAttribute");
1287+
};
1288+
let JSXAttributeName::Identifier(name) = &attr.name else {
1289+
panic!("expected ident name");
1290+
};
1291+
let Some(JSXAttributeValue::ExpressionContainer(container)) = &attr.value else {
1292+
panic!("expected expression container value");
1293+
};
1294+
1295+
assert_eq!(attr.span.source_text(source), "{prop}");
1296+
assert_eq!(container.span.source_text(source), "{prop}");
1297+
assert_eq!(name.span.source_text(source), "prop");
1298+
}
1299+
1300+
/// Shorthand is desugared away, so this span relationship is all consumers have to detect it.
1301+
#[test]
1302+
fn parse_astro_attribute_shorthand_is_distinguishable_by_span() {
1303+
let shorthand =
1304+
["<C {prop} />", "<C {prop} />", "<C {{ a: 1 }} />", "<C {obj?.prop} />"];
1305+
let longhand = [
1306+
"<C class=\"a\" />",
1307+
"<C class=\"a\" />",
1308+
"<C\n class=\"a\"\n/>",
1309+
"<C client:load />",
1310+
"<C a:b=\"c\" />",
1311+
"<C disabled />",
1312+
"<C x={y} />",
1313+
"<C x=`y` />",
1314+
];
1315+
1316+
for (source, expected) in
1317+
shorthand.iter().map(|s| (s, true)).chain(longhand.iter().map(|s| (s, false)))
1318+
{
1319+
let allocator = Allocator::default();
1320+
let ret = Parser::new(&allocator, source, SourceType::astro()).parse_astro();
1321+
assert!(!ret.panicked, "{source:?} panicked: {:?}", ret.errors);
1322+
assert!(ret.errors.is_empty(), "{source:?} errors: {:?}", ret.errors);
1323+
1324+
let JSXChild::Element(element) = &ret.root.body[0] else {
1325+
panic!("{source:?}: expected JSXChild::Element");
1326+
};
1327+
let JSXAttributeItem::Attribute(attr) = &element.opening_element.attributes[0] else {
1328+
panic!("{source:?}: expected Attribute");
1329+
};
1330+
let JSXAttributeName::Identifier(name) = &attr.name else {
1331+
panic!("{source:?}: expected ident name");
1332+
};
1333+
1334+
assert_eq!(
1335+
attr.span.start < name.span.start,
1336+
expected,
1337+
"{source:?}: attribute span {:?} vs name span {:?}",
1338+
attr.span,
1339+
name.span
1340+
);
1341+
if expected {
1342+
assert_eq!(&source[attr.span.start as usize..][..1], "{", "{source:?}");
1343+
}
1344+
}
1345+
}
1346+
12711347
// A non-identifier `{expr}` is a shorthand whose name is the expression source text.
12721348
#[test]
12731349
fn parse_astro_attribute_shorthand_object_expression() {

0 commit comments

Comments
 (0)