diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index e17414032..0df3d6d37 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -42,7 +42,7 @@ macro_rules! node { impl<'a> AstNode<'a> for $name<'a> { #[inline] fn from_untyped(node: &'a SyntaxNode) -> Option { - if matches!(node.kind(), SyntaxKind::$name) { + if node.kind() == SyntaxKind::$name { Some(Self(node)) } else { Option::None @@ -1440,6 +1440,18 @@ impl BinOp { }) } + /// Whether this is an assignment operator. + pub fn is_assignment(self) -> bool { + matches!( + self, + Self::Assign + | Self::AddAssign + | Self::SubAssign + | Self::MulAssign + | Self::DivAssign + ) + } + /// The precedence of this operator. pub fn precedence(self) -> usize { match self { diff --git a/crates/typst/src/eval/func.rs b/crates/typst/src/eval/func.rs index fcde26d7e..b7951cefa 100644 --- a/crates/typst/src/eval/func.rs +++ b/crates/typst/src/eval/func.rs @@ -638,6 +638,11 @@ impl<'a> CapturesVisitor<'a> { self.internal.exit(); } + // Don't capture the field of a field access. + Some(ast::Expr::FieldAccess(access)) => { + self.visit(access.target().to_untyped()); + } + // A closure contains parameter bindings, which are bound before the // body is evaluated. Care must be taken so that the default values // of named parameters cannot access previous parameter bindings. @@ -683,6 +688,16 @@ impl<'a> CapturesVisitor<'a> { } } + // Don't capture left-hand side of an assignment. + Some(ast::Expr::Binary(binary)) if binary.op().is_assignment() => { + self.visit(binary.rhs().to_untyped()); + } + + // Don't capture the left-hand side of a destructuring assignment. + Some(ast::Expr::DestructAssign(assignment)) => { + self.visit(assignment.value().to_untyped()); + } + // A for loop contains one or two bindings in its pattern. These are // active after the iterable is evaluated but before the body is // evaluated. @@ -710,8 +725,14 @@ impl<'a> CapturesVisitor<'a> { } } - // Everything else is traversed from left to right. _ => { + // Never capture the name part of a named pair. + if let Some(named) = node.cast::() { + self.visit(named.expr().to_untyped()); + return; + } + + // Everything else is traversed from left to right. for child in node.children() { self.visit(child); } @@ -800,5 +821,12 @@ mod tests { // Blocks. test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]); test("#[#let x = 1]#x", &["x"]); + + // Field access. + test("#foo(body: 1)", &[]); + test("#(body: 1)", &[]); + test("#(body = 1)", &[]); + test("#(body += y)", &["y"]); + test("#{ (body, a) = (y, 1) }", &["y"]); } }