Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions crates/mq-formatter/src/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,21 @@ impl Formatter {
});
}

fn needs_descendant_space(
prev: &mq_lang::Shared<mq_lang::CstNode>,
current: &mq_lang::Shared<mq_lang::CstNode>,
) -> bool {
let selector_of = |n: &mq_lang::Shared<mq_lang::CstNode>| {
n.token.as_ref().and_then(|t| mq_lang::Selector::try_from(&**t).ok())
};

match selector_of(current) {
Some(selector) if selector.is_attribute_selector() => false,
Some(mq_lang::Selector::Property(_)) => !matches!(selector_of(prev), Some(mq_lang::Selector::Property(_))),
_ => true,
}
}

fn format_block(
&mut self,
node: &mq_lang::Shared<mq_lang::CstNode>,
Expand All @@ -699,11 +714,18 @@ impl Formatter {
child.token.as_ref().map(|t| &t.kind),
Some(mq_lang::TokenKind::Selector(s)) if s == "."
);

if is_list_iterator {
child.children.iter().for_each(|bracket_child| {
self.format_node(mq_lang::Shared::clone(bracket_child), 0);
});
} else {
if i > 0
&& Self::needs_descendant_space(&node.children[i - 1], child)
&& !self.output.ends_with(' ')
{
self.append_space();
}
self.format_node(mq_lang::Shared::clone(child), if i == 0 { indent_level } else { 0 });
}
});
Expand Down Expand Up @@ -2935,6 +2957,12 @@ end
#[case::selector_call_heading_multi_arg(".h(1,2)", ".h(1, 2)")]
#[case::selector_call_code_lang(".code(\"rust\")", ".code(\"rust\")")]
#[case::selector_call_pipe(".h(1)|.text()", ".h(1) | .text()")]
#[case::descendant_chain(".blockquote .code", ".blockquote .code")]
#[case::descendant_chain_no_space(".blockquote.code", ".blockquote .code")]
#[case::descendant_chain_extra_space(".blockquote .code", ".blockquote .code")]
#[case::descendant_chain_three_levels(".blockquote .list .code", ".blockquote .list .code")]
#[case::descendant_chain_with_selector_call(".blockquote .code(\"rust\")", ".blockquote .code(\"rust\")")]
#[case::descendant_chain_trailing_attr(".blockquote .code.lang", ".blockquote .code.lang")]
#[case::arrow_simple("->(): program;", "->(): program;")]
#[case::arrow_multiline(
"->(arg1,arg2):
Expand Down
13 changes: 13 additions & 0 deletions crates/mq-hir/src/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,19 @@ mod tests {
assert!(hir.find_symbol_in_position(source_id, pos).is_some());
}

#[test]
fn test_find_symbol_in_position_resolves_descendant_chain_step() {
// Regression: a synthetic `..` bridge node used to shadow `.code`'s range.
let mut hir = Hir::default();
let (source_id, _) = hir.add_code(None, ".blockquote .code");
let pos = mq_lang::Position::new(1, 14);

let (_, symbol) = hir
.find_symbol_in_position(source_id, pos)
.expect("symbol at .code position");
assert_eq!(symbol.value.as_deref(), Some(".code"));
}

#[test]
fn test_find_scope_in_position() {
let mut hir = Hir::default();
Expand Down
177 changes: 161 additions & 16 deletions crates/mq-lang/src/ast/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2553,14 +2553,65 @@ impl<'a, 'alloc> Parser<'a, 'alloc> {
}
}

/// Parse a selector with an attribute suffix and convert it to an attr() function call
fn parse_selector_with_attribute(
/// Consumes any selector token(s) following an already-parsed `base_node`.
fn parse_selector_tail(
&mut self,
token: &Shared<Token>,
attr_token: Shared<Token>,
base_node: Shared<Node>,
) -> Result<Shared<Node>, SyntaxError> {
let base_node = self.parse_selector_direct(token)?;
self.build_attr_call_for_node(base_node, attr_token, token)
if !self.is_next_token(|kind| matches!(kind, TokenKind::Selector(_))) {
return Ok(base_node);
}
let next_token = Shared::clone(self.tokens.next().unwrap());
let selector = Selector::try_from(&*next_token).map_err(SyntaxError::UnknownSelector)?;

if selector.is_attribute_selector() {
return self.build_attr_call_for_node(base_node, next_token, token);
}

// Descendant chain: `.blockquote .code` → Block([base, .., .code, ...])
let mut nodes: Program = vec![base_node];
let mut step_token = next_token;
let mut step_selector = selector;

loop {
nodes.push(Shared::new(Node {
token_id: self.token_arena.alloc(Shared::clone(token)),
expr: Shared::new(Expr::Selector(Selector::Recursive)),
}));

let step_expr = if self.is_next_token(|kind| matches!(kind, TokenKind::LParen)) {
Expr::SelectorCall(step_selector, self.parse_args()?)
} else {
Expr::Selector(step_selector)
};
nodes.push(Shared::new(Node {
token_id: self.token_arena.alloc(Shared::clone(&step_token)),
expr: Shared::new(step_expr),
}));

if !self.is_next_token(|kind| matches!(kind, TokenKind::Selector(_))) {
break;
}
let peeked_token = Shared::clone(self.tokens.next().unwrap());
let peeked_selector = Selector::try_from(&*peeked_token).map_err(SyntaxError::UnknownSelector)?;

if peeked_selector.is_attribute_selector() {
let chained = Shared::new(Node {
token_id: self.token_arena.alloc(Shared::clone(token)),
expr: Shared::new(Expr::Block(nodes)),
});
return self.build_attr_call_for_node(chained, peeked_token, token);
}

step_token = peeked_token;
step_selector = peeked_selector;
}

Ok(Shared::new(Node {
token_id: self.token_arena.alloc(Shared::clone(token)),
expr: Shared::new(Expr::Block(nodes)),
}))
}

/// Parse a selector without checking for attributes (to avoid infinite recursion)
Expand Down Expand Up @@ -2682,10 +2733,9 @@ impl<'a, 'alloc> Parser<'a, 'alloc> {
}
}

if self.is_next_token(|kind| matches!(kind, TokenKind::Selector(_)))
&& let Some(attr_token) = self.tokens.next()
{
return self.parse_selector_with_attribute(token, Shared::clone(attr_token));
if self.is_next_token(|kind| matches!(kind, TokenKind::Selector(_))) {
let base_node = self.parse_selector_direct(token)?;
return self.parse_selector_tail(token, base_node);
}

// Check for selector call: `.h(...)`, `.code(...)`
Expand All @@ -2700,13 +2750,8 @@ impl<'a, 'alloc> Parser<'a, 'alloc> {
token_id: self.token_arena.alloc(Shared::clone(token)),
expr: Shared::new(Expr::SelectorCall(selector, args)),
});
// Check for attribute access on SelectorCall: `.h(1).level`
if self.is_next_token(|kind| matches!(kind, TokenKind::Selector(_)))
&& let Some(attr_token) = self.tokens.next()
{
return self.build_attr_call_for_node(base_node, Shared::clone(attr_token), token);
}
return Ok(base_node);
// Check for attribute access or a descendant chain continuation: `.h(1).level`, `.h(1) .code`
return self.parse_selector_tail(token, base_node);
}
}

Expand Down Expand Up @@ -9076,6 +9121,106 @@ mod tests {
}
}

#[rstest]
// Two selectors: base, then a single descendant hop.
#[case::two_levels(vec![".blockquote", ".code"], vec![Selector::Blockquote, Selector::Code])]
// Three selectors: two descendant hops.
#[case::three_levels(
vec![".blockquote", ".list", ".code"],
vec![Selector::Blockquote, Selector::List(None, None), Selector::Code]
)]
fn test_parse_selector_descendant_chain(#[case] selectors: Vec<&str>, #[case] expected_selectors: Vec<Selector>) {
let mut arena = Arena::new(10);
let mut tokens = selectors
.iter()
.map(|selector| {
Shared::new(Token {
range: Range::default(),
kind: TokenKind::Selector(SmolStr::new(selector)),
module_id: 1.into(),
})
})
.collect::<Vec<_>>();
tokens.push(Shared::new(Token {
range: Range::default(),
kind: TokenKind::Eof,
module_id: 1.into(),
}));

let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse();

match result {
Ok(program) => {
assert_eq!(program.len(), 1);
if let Expr::Block(nodes) = &*program[0].expr {
// Desugars to `base | .. | step1 | .. | step2 | ...`, i.e. a plain
// selector interleaved with a `Recursive` selector between each hop.
let expected: Vec<Selector> = expected_selectors
.into_iter()
.enumerate()
.flat_map(|(i, sel)| {
if i == 0 {
vec![sel]
} else {
vec![Selector::Recursive, sel]
}
})
.collect();
assert_eq!(nodes.len(), expected.len());
for (node, expected_sel) in nodes.iter().zip(expected.iter()) {
if let Expr::Selector(sel) = &*node.expr {
assert_eq!(sel, expected_sel);
} else {
panic!("Expected Selector expression, got {:?}", node.expr);
}
}
} else {
panic!("Expected Block expression, got {:?}", program[0].expr);
}
}
Err(err) => panic!("Parse error: {:?}", err),
}
}

#[test]
fn test_parse_selector_descendant_chain_trailing_attribute() {
// `.blockquote .code.lang` → attr(Block([.blockquote, .., .code]), "lang")
let mut arena = Arena::new(10);
let selectors = [".blockquote", ".code", ".lang"];
let mut tokens = selectors
.iter()
.map(|selector| {
Shared::new(Token {
range: Range::default(),
kind: TokenKind::Selector(SmolStr::new(*selector)),
module_id: 1.into(),
})
})
.collect::<Vec<_>>();
tokens.push(Shared::new(Token {
range: Range::default(),
kind: TokenKind::Eof,
module_id: 1.into(),
}));

let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse();

match result {
Ok(program) => {
assert_eq!(program.len(), 1);
if let Expr::Call(ident, args) = &*program[0].expr {
assert_eq!(ident.name, "attr".into());
assert_eq!(args.len(), 2);
assert!(matches!(&*args[0].expr, Expr::Block(nodes) if nodes.len() == 3));
assert!(matches!(&*args[1].expr, Expr::Literal(Literal::String(s)) if s == "lang"));
} else {
panic!("Expected Call expression, got {:?}", program[0].expr);
}
}
Err(err) => panic!("Parse error: {:?}", err),
}
}

#[rstest]
#[case::match_simple_literal(
vec![
Expand Down
78 changes: 78 additions & 0 deletions crates/mq-lang/src/cst/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,57 @@ impl<'a> Parser<'a> {
{
node.children =
vec![self.next_node(|kind| matches!(kind, TokenKind::Selector(_)), NodeKind::Selector)?];
return Ok(Shared::new(node));
}

if let Some(next_token) = self.peek()
&& next_token.is_selector()
&& matches!(Selector::try_from(&**next_token), Ok(sel) if !sel.is_attribute_selector())
{
let first_token = Shared::clone(node.token.as_ref().unwrap());
let block_leading = node.leading_trivia.clone();
let mut nodes = vec![Shared::new(node)];

while let Some(step_token) = self.peek() {
if !step_token.is_selector() {
break;
}

match Selector::try_from(&**step_token) {
Ok(sel) if sel.is_attribute_selector() => {
nodes.push(
self.next_node(|kind| matches!(kind, TokenKind::Selector(_)), NodeKind::Selector)?,
);
break;
}
Ok(_) => {
let leading_trivia = self.parse_leading_trivia();
let step_src_token = self.next_token(|kind| matches!(kind, TokenKind::Selector(_)))?;
let trailing_trivia = self.parse_trailing_trivia();
let mut step_node = Node {
kind: NodeKind::Selector,
token: Some(Shared::clone(&step_src_token)),
leading_trivia,
trailing_trivia,
children: Vec::new(),
};
if self.try_next_token(|kind| matches!(kind, TokenKind::LParen)) {
step_node.kind = NodeKind::SelectorCall;
step_node.children = self.parse_args()?;
}
nodes.push(Shared::new(step_node));
}
Err(_) => break,
}
}

return Ok(Shared::new(Node {
kind: NodeKind::Block,
token: Some(first_token),
leading_trivia: block_leading,
trailing_trivia: Vec::new(),
children: nodes,
}));
}

Ok(Shared::new(node))
Expand Down Expand Up @@ -11362,6 +11413,33 @@ mod tests {
assert_eq!(nodes, expected.0);
}

#[test]
fn test_descendant_chain_has_no_synthetic_bridge_node() {
// No synthetic DoubleDot node: it would duplicate the next selector's source range.
let (nodes, errors) = crate::parse_recovery(".blockquote .code");
assert!(!errors.has_errors());

let block = nodes
.iter()
.find(|node| node.kind == NodeKind::Block)
.expect("descendant chain should parse as a Block");

assert_eq!(
block.children.len(),
2,
"expected exactly the two real selectors, got {:#?}",
block.children
);
assert!(
block
.children
.iter()
.all(|child| !matches!(child.token.as_ref().map(|t| &t.kind), Some(TokenKind::DoubleDot))),
"no child should carry a synthetic DoubleDot token: {:#?}",
block.children
);
}

#[test]
fn test_unmatched_end_error_message() {
// Verify the error message text for `UnmatchedEnd`.
Expand Down
Loading