Repository: martinohmann/hcl-rs (rust)

Issue:
[bug]: multiline ternary throws parser error
### Description of the bug

When trying this against a codebase I got this error even though this works fine with terraform (which hopefully follows the standard).

### Steps To Reproduce

tf file:
```tf
resource "google_pubsub_subscription" "to_ctrl_subscriptions" {
  topic   = "pre-prod-${each.value.tenant}+${each.value.space}-v1-${
    each.value.tenant == "tenant1" && each.value.space == "1" ? "2331240" :
    each.value.tenant == "tenant2" && each.value.space == "3" ? "9231231" :
    each.value.tenant == "tenant3" && each.value.space == "2" ? "r32345d0" :
    each.value.tenant == "tenant4" && each.value.space == "4" ? "123jfvri4" : ""
  }"
}
```
error:
```
Error discovering modules: Failed to parse HCL in module 'pubsub/modules/pubsub':    --> HCL parse error in line 201, column 71
    |
201 |     each.value.tenant == "tenant1" && each.value.space == "1" ? "2331240" :
    |                                                                                                                                ^---
    |
    = invalid expression; expected `"`, `[`, `{`, `-`, `!`, `(`, `_`, `<`, letter or digit
```

### Additional Information

_No response_

Pull request description:
fix(parser): allow multiline expressions in more places
Fixes #426.

I also found another case where multiline expressions are valid but the parser didn't handle so far and fixed that along the way.

Also contains some `rustfmt` changes.

Patch:
diff --git a/crates/hcl-edit/src/parser/expr.rs b/crates/hcl-edit/src/parser/expr.rs
index 8050906..e2f233a 100644
--- a/crates/hcl-edit/src/parser/expr.rs
+++ b/crates/hcl-edit/src/parser/expr.rs
@@ -40,6 +40,12 @@ pub(super) fn expr(input: &mut Input) -> ModalResult<Expression> {
     parse_expr(RefCell::default(), input)
 }
 
+pub(super) fn multiline_expr(input: &mut Input) -> ModalResult<Expression> {
+    let mut state = ExprParseState::default();
+    state.allow_newlines(true);
+    parse_expr(RefCell::new(state), input)
+}
+
 fn expr_with_state<'i>(
     state: &RefCell<ExprParseState>,
 ) -> impl ModalParser<Input<'i>, Expression, ContextError> + '_ {
@@ -230,7 +236,7 @@ fn traversal_operator(input: &mut Input) -> ModalResult<TraversalOperator> {
                 ws,
                 dispatch! {peek(any);
                     '*' => '*'.value(TraversalOperator::FullSplat(Decorated::new(Splat))),
-                    _ => expr.map(TraversalOperator::Index),
+                    _ => multiline_expr.map(TraversalOperator::Index),
                 },
                 ws,
             ),
@@ -603,10 +609,9 @@ fn parenthesis<'i>(
     state: &RefCell<ExprParseState>,
 ) -> impl ModalParser<Input<'i>, (), ContextError> + '_ {
     move |input: &mut Input<'i>| {
-        state.borrow_mut().allow_newlines(true);
         delimited(
             cut_char('('),
-            decorated(ws, expr_with_state(state), ws)
+            decorated(ws, multiline_expr, ws)
                 .map(|expr| state.borrow_mut().on_expr_term(Parenthesis::new(expr))),
             cut_char(')'),
         )
diff --git a/crates/hcl-edit/src/parser/string.rs b/crates/hcl-edit/src/parser/string.rs
index e8c2a26..74b89c3 100644
--- a/crates/hcl-edit/src/parser/string.rs
+++ b/crates/hcl-edit/src/parser/string.rs
@@ -34,13 +34,11 @@ where
 
         loop {
             match fragment_parser.parse_next(input) {
-                Ok(fragment) => {
-                    match fragment {
-                        StringFragment::Literal(s) => string.to_mut().push_str(s),
-                        StringFragment::EscapedChar(c) => string.to_mut().push(c),
-                        StringFragment::EscapedMarker(m) => string.to_mut().push_str(m.unescape()),
-                    }
-                }
+                Ok(fragment) => match fragment {
+                    StringFragment::Literal(s) => string.to_mut().push_str(s),
+                    StringFragment::EscapedChar(c) => string.to_mut().push(c),
+                    StringFragment::EscapedMarker(m) => string.to_mut().push_str(m.unescape()),
+                },
                 Err(_) => return Ok(string),
             }
         }
diff --git a/crates/hcl-edit/src/parser/template.rs b/crates/hcl-edit/src/parser/template.rs
index aef9405..f756d21 100644
--- a/crates/hcl-edit/src/parser/template.rs
+++ b/crates/hcl-edit/src/parser/template.rs
@@ -1,6 +1,6 @@
 use super::prelude::*;
 
-use super::expr::expr;
+use super::expr::{expr, multiline_expr};
 use super::repr::{decorated, spanned};
 use super::string::{
     build_string, cut_char, cut_ident, cut_tag, quoted_string_fragment, raw_string,
@@ -96,7 +96,7 @@ where
 }
 
 fn interpolation(input: &mut Input) -> ModalResult<Interpolation> {
-    control("${", decorated(ws, expr, ws))
+    control("${", decorated(ws, multiline_expr, ws))
         .map(|(expr, strip)| {
             let mut interp = Interpolation::new(expr);
             interp.strip = strip;


Test patch:
diff --git a/crates/hcl-edit/tests/regressions.rs b/crates/hcl-edit/tests/regressions.rs
index eb64da5..d100d09 100644
--- a/crates/hcl-edit/tests/regressions.rs
+++ b/crates/hcl-edit/tests/regressions.rs
@@ -4,6 +4,18 @@ use hcl_edit::template::{Element, Interpolation, Template};
 use hcl_edit::{Ident, Span};
 use pretty_assertions::assert_eq;
 
+macro_rules! assert_ok {
+    ($input:expr) => {
+        assert!($input.parse::<Body>().is_ok());
+    };
+}
+
+macro_rules! assert_err {
+    ($input:expr) => {
+        assert!($input.parse::<Body>().is_err());
+    };
+}
+
 // https://github.com/martinohmann/hcl-rs/issues/248
 #[test]
 fn issue_248() {
@@ -101,20 +113,9 @@ fn issue_294() {
 }
 
 // https://github.com/martinohmann/hcl-rs/issues/319
+// https://github.com/martinohmann/hcl-rs/issues/426
 #[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());
-        };
-    }
-
+fn issues_319_426() {
     // single line expressions with parenthesis
     assert_ok! {r#"
         foo = (true ? "bar" : "baz")
@@ -164,6 +165,24 @@ fn issue_319() {
             .foo
             [2]
     "#};
+
+    // multiline expressions in traversal index operator
+    assert_ok! {r#"
+        foo = var.foo[
+            bar &&
+            baz ? 0 :
+            1
+        ]
+    "#};
+
+    // multiline expressions in template string interpolation
+    assert_ok! {r#"
+        foo = "template-string-${
+            bar &&
+            baz ? "qux" :
+            ""
+        }"
+    "#};
 }
 
 // https://github.com/martinohmann/hcl-rs/issues/350
@@ -183,18 +202,6 @@ fn issue_350() {
 // https://github.com/martinohmann/hcl-rs/issues/367
 #[test]
 fn issue_367() {
-    macro_rules! assert_ok {
-        ($input:expr) => {
-            assert!($input.parse::<Body>().is_ok());
-        };
-    }
-
-    macro_rules! assert_err {
-        ($input:expr) => {
-            assert!($input.parse::<Body>().is_err());
-        };
-    }
-
     // multiline expressions with function calls
     assert_ok! {r#"
         foo = length(