Repository: martinohmann/hcl-rs
Pull request #330 (2024-04-08 10:38:10)

fix(parser): allow newlines in parenthesis expressions
Fixes https://github.com/martinohmann/hcl-rs/issues/319


Patch:
diff --git a/crates/hcl-edit/src/parser/expr.rs b/crates/hcl-edit/src/parser/expr.rs
index 7d086e5..f825d76 100644
--- a/crates/hcl-edit/src/parser/expr.rs
+++ b/crates/hcl-edit/src/parser/expr.rs
@@ -26,11 +26,32 @@ use winnow::combinator::{
 };
 use winnow::token::{any, none_of, one_of, take};
 
+fn ws_or_sp<'i, 's>(
+    state: &'s RefCell<ExprParseState>,
+) -> impl Parser<Input<'i>, (), ContextError> + 's {
+    move |input: &mut Input<'i>| {
+        if state.borrow().newlines_allowed() {
+            ws.parse_next(input)
+        } else {
+            sp.parse_next(input)
+        }
+    }
+}
+
 pub(super) fn expr(input: &mut Input) -> PResult<Expression> {
-    let state = RefCell::new(ExprParseState::default());
+    parse_expr(RefCell::default(), input)
+}
+
+fn expr_with_state<'i, 's>(
+    state: &'s RefCell<ExprParseState>,
+) -> impl Parser<Input<'i>, Expression, ContextError> + 's {
+    move |input: &mut Input<'i>| parse_expr(state.clone(), input)
+}
+
+#[inline]
+fn parse_expr(state: RefCell<ExprParseState>, input: &mut Input) -> PResult<Expression> {
     expr_inner(&state).parse_next(input)?;
-    let expr = state.into_inner().into_expr();
-    Ok(expr)
+    Ok(state.into_inner().into_expr())
 }
 
 fn expr_inner<'i, 's>(
@@ -45,7 +66,7 @@ fn expr_inner<'i, 's>(
             // Parse the next whitespace sequence and only add it as decor suffix to the expression if
             // we actually encounter a traversal, conditional or binary operation. We'll rewind the
             // parser if none of these follow.
-            let suffix = sp.span().parse_next(input)?;
+            let suffix = ws_or_sp(state).span().parse_next(input)?;
 
             // Peek the next two bytes to identify the following operation, if any.
             match peek(take::<_, _, ContextError>(2usize)).parse_next(input) {
@@ -182,7 +203,7 @@ fn traversal<'i, 's>(
     move |input: &mut Input<'i>| {
         repeat(
             1..,
-            prefix_decorated(sp, traversal_operator.map(Decorated::new)),
+            prefix_decorated(ws_or_sp(state), traversal_operator.map(Decorated::new)),
         )
         .map(|operators| state.borrow_mut().on_traversal(operators))
         .parse_next(input)
@@ -249,7 +270,7 @@ fn binary_op<'i, 's>(
     move |input: &mut Input<'i>| {
         (
             spanned(binary_operator.map(Spanned::new)),
-            prefix_decorated(sp, expr),
+            prefix_decorated(ws_or_sp(state), expr_with_state(state)),
         )
             .map(|(operator, rhs_expr)| state.borrow_mut().on_binary_op(operator, rhs_expr))
             .parse_next(input)
@@ -285,8 +306,14 @@ fn conditional<'i, 's>(
 ) -> impl Parser<Input<'i>, (), ContextError> + 's {
     move |input: &mut Input<'i>| {
         (
-            preceded(b'?', decorated(sp, expr, sp)),
-            preceded(cut_char(':'), prefix_decorated(sp, expr)),
+            preceded(
+                b'?',
+                decorated(ws_or_sp(state), expr_with_state(state), ws_or_sp(state)),
+            ),
+            preceded(
+                cut_char(':'),
+                prefix_decorated(ws_or_sp(state), expr_with_state(state)),
+            ),
         )
             .map(|(true_expr, false_expr)| state.borrow_mut().on_conditional(true_expr, false_expr))
             .parse_next(input)
@@ -555,9 +582,10 @@ fn parenthesis<'i, 's>(
     state: &'s RefCell<ExprParseState>,
 ) -> impl Parser<Input<'i>, (), ContextError> + 's {
     move |input: &mut Input<'i>| {
+        state.borrow_mut().allow_newlines(true);
         delimited(
             cut_char('('),
-            decorated(ws, expr, ws)
+            decorated(ws, expr_with_state(state), ws)
                 .map(|expr| state.borrow_mut().on_expr_term(Parenthesis::new(expr))),
             cut_char(')'),
         )
diff --git a/crates/hcl-edit/src/parser/state.rs b/crates/hcl-edit/src/parser/state.rs
index 7742d88..728b2d4 100644
--- a/crates/hcl-edit/src/parser/state.rs
+++ b/crates/hcl-edit/src/parser/state.rs
@@ -66,6 +66,7 @@ pub(super) struct ExprParseState {
     unary: Option<Spanned<UnaryOperator>>,
     current: Option<Expression>,
     ws: Option<Range<usize>>,
+    allow_newlines: bool,
 }
 
 impl ExprParseState {
@@ -143,7 +144,26 @@ impl ExprParseState {
         self.current = Some(expr);
     }
 
+    pub(super) fn allow_newlines(&mut self, allow: bool) {
+        self.allow_newlines = allow;
+    }
+
+    pub(super) fn newlines_allowed(&self) -> bool {
+        self.allow_newlines
+    }
+
     pub(super) fn into_expr(self) -> Expression {
         self.current.unwrap()
     }
 }
+
+impl Clone for ExprParseState {
+    fn clone(&self) -> Self {
+        ExprParseState {
+            unary: None,
+            current: None,
+            ws: None,
+            allow_newlines: self.allow_newlines,
+        }
+    }
+}


Test patch:
diff --git a/crates/hcl-edit/tests/regressions.rs b/crates/hcl-edit/tests/regressions.rs
index 400aa31..4ff5342 100644
--- a/crates/hcl-edit/tests/regressions.rs
+++ b/crates/hcl-edit/tests/regressions.rs
@@ -99,3 +99,69 @@ fn issue_294() {
         );
     }
 }
+
+// https://github.com/martinohmann/hcl-rs/issues/319
+#[test]
+fn issue_319() {
+    macro_rules! assert_ok {
+        ($input:expr) => {
+            assert!($input.parse::<Body>().is_ok());
+        };
+    }
+
+    macro_rules! assert_err {
+        ($input:expr) => {
+            assert!($input.parse::<Body>().is_err());
+        };
+    }
+
+    // single line expressions with parenthesis
+    assert_ok! {r#"
+        foo = (true ? "bar" : "baz")
+    "#};
+    assert_ok! {r#"
+        foo = (1 > 2)
+    "#};
+    assert_ok! {r#"
+        foo = (var.foo[2])
+    "#};
+
+    // multiline expressions with parenthesis
+    assert_ok! {r#"
+        foo = (true ?
+            "bar" :
+            "baz"
+        )
+    "#};
+    assert_ok! {r#"
+        foo = (
+            1
+            >
+            2
+        )
+    "#};
+    assert_ok! {r#"
+        foo = (
+            var
+                .foo
+                [2]
+        )
+    "#};
+
+    // invalid multiline expressions without parenthesis
+    assert_err! {r#"
+        foo = true ?
+            "bar" :
+            "baz"
+    "#};
+    assert_err! {r#"
+        foo = 1
+            >
+            2
+    "#};
+    assert_err! {r#"
+        foo = var
+            .foo
+            [2]
+    "#};
+}