Repository: martinohmann/hcl-rs
Pull request #222 (2023-05-06 11:09:58)

fix(parser): error on duplicate attributes


Patch:
diff --git a/crates/hcl-edit/Cargo.toml b/crates/hcl-edit/Cargo.toml
index 9270293..cad12f5 100644
--- a/crates/hcl-edit/Cargo.toml
+++ b/crates/hcl-edit/Cargo.toml
@@ -30,6 +30,7 @@ default = []
 perf = ["hcl-primitives/perf"]
 
 [dependencies]
+fnv = "1.0.7"
 hcl-primitives = { version = "0.1.0", path = "../hcl-primitives" }
 vecmap-rs = "0.1.10"
 winnow = "= 0.4.4"
diff --git a/crates/hcl-edit/src/parser/context.rs b/crates/hcl-edit/src/parser/context.rs
index 10a7846..a317961 100644
--- a/crates/hcl-edit/src/parser/context.rs
+++ b/crates/hcl-edit/src/parser/context.rs
@@ -1,4 +1,8 @@
-use super::{error::ParseError, string::ident, IResult, Input};
+use super::{
+    error::ParseError,
+    string::{ident, str_ident},
+    IResult, Input,
+};
 use crate::{repr::Decorated, Ident};
 use std::fmt;
 use winnow::{combinator::cut_err, stream::AsChar, Parser};
@@ -47,3 +51,9 @@ pub(super) fn cut_ident(input: Input) -> IResult<Input, Decorated<Ident>> {
         .context(Context::Expected(Expected::Description("identifier")))
         .parse_next(input)
 }
+
+pub(super) fn cut_str_ident(input: Input) -> IResult<Input, &str> {
+    cut_err(str_ident)
+        .context(Context::Expected(Expected::Description("identifier")))
+        .parse_next(input)
+}
diff --git a/crates/hcl-edit/src/parser/state.rs b/crates/hcl-edit/src/parser/state.rs
index 74827c8..ad0d402 100644
--- a/crates/hcl-edit/src/parser/state.rs
+++ b/crates/hcl-edit/src/parser/state.rs
@@ -7,16 +7,22 @@ use crate::{
     structure::{Body, Structure},
     RawString,
 };
+use fnv::FnvHashSet;
 use std::ops::Range;
 
 #[derive(Debug, Default)]
-pub(super) struct BodyParseState {
+pub(super) struct BodyParseState<'a> {
+    attribute_keys: FnvHashSet<&'a str>,
     current: Option<Structure>,
     structures: Vec<Structure>,
     ws: Option<Range<usize>>,
 }
 
-impl BodyParseState {
+impl<'a> BodyParseState<'a> {
+    pub(super) fn is_redefined(&mut self, key: &'a str) -> bool {
+        !self.attribute_keys.insert(key)
+    }
+
     pub(super) fn on_ws(&mut self, span: Range<usize>) {
         if let Some(existing) = &self.ws {
             self.ws = Some(existing.start..span.end);
diff --git a/crates/hcl-edit/src/parser/structure.rs b/crates/hcl-edit/src/parser/structure.rs
index 3becb2c..005eaf9 100644
--- a/crates/hcl-edit/src/parser/structure.rs
+++ b/crates/hcl-edit/src/parser/structure.rs
@@ -1,5 +1,5 @@
 use super::{
-    context::{cut_char, cut_ident, Context, Expected},
+    context::{cut_char, cut_str_ident, Context, Expected},
     expr::expr,
     repr::{decorated, prefix_decorated, suffix_decorated},
     state::BodyParseState,
@@ -12,6 +12,7 @@ use crate::{
     repr::{Decorate, Decorated, SetSpan},
     structure::{Attribute, Block, BlockLabel, Body, Structure},
 };
+use hcl_primitives::Ident;
 use std::cell::RefCell;
 use winnow::{
     ascii::line_ending,
@@ -51,22 +52,37 @@ pub(super) fn body(input: Input) -> IResult<Input, Body> {
 }
 
 fn structure<'i, 's>(
-    state: &'s RefCell<BodyParseState>,
+    state: &'s RefCell<BodyParseState<'i>>,
 ) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, ()> + 's {
     move |input: Input<'i>| {
         let start = input.location();
+        let initial_input = input.clone();
         let (input, _) = peek(one_of(is_id_start)).parse_next(input)?;
-        let (input, ident) = suffix_decorated(cut_ident, sp).parse_next(input)?;
+        let (input, ident) = cut_str_ident.parse_next(input)?;
+        let (input, suffix) = raw_string(sp).parse_next(input)?;
         let (input, ch) = peek(any).parse_next(input)?;
 
         let (input, mut structure) = match ch {
             b'=' => {
+                if state.borrow_mut().is_redefined(ident) {
+                    return cut_err(fail)
+                        .context(Context::Expression("attribute"))
+                        .context(Context::Expected(Expected::Description(
+                            "unique attribute key; found redefined attribute",
+                        )))
+                        .parse_next(initial_input);
+                }
+
                 let (input, expr) = attribute_expr(input)?;
+                let mut ident = Decorated::new(Ident::new_unchecked(ident));
+                ident.decor_mut().set_suffix(suffix);
                 let attr = Attribute::new(ident, expr);
                 (input, Structure::Attribute(attr))
             }
             b'{' => {
                 let (input, body) = block_body(input)?;
+                let mut ident = Decorated::new(Ident::new_unchecked(ident));
+                ident.decor_mut().set_suffix(suffix);
                 let mut block = Block::new(ident);
                 block.body = body;
                 (input, Structure::Block(block))
@@ -74,6 +90,8 @@ fn structure<'i, 's>(
             ch if ch == b'"' || is_id_start(ch) => {
                 let (input, labels) = block_labels(input)?;
                 let (input, body) = block_body(input)?;
+                let mut ident = Decorated::new(Ident::new_unchecked(ident));
+                ident.decor_mut().set_suffix(suffix);
                 let mut block = Block::new(ident);
                 block.body = body;
                 block.labels = labels;


Test patch:
diff --git a/crates/hcl-edit/tests/parse.rs b/crates/hcl-edit/tests/parse.rs
index 4cd3e9c..ff2fc47 100644
--- a/crates/hcl-edit/tests/parse.rs
+++ b/crates/hcl-edit/tests/parse.rs
@@ -23,6 +23,17 @@ fn invalid_structures() {
               |
               = invalid structure; expected `{`, `=`, `"` or identifier"#}
     );
+
+    assert_error!(
+        "foo = 1\nfoo = 2",
+        indoc! {r#"
+             --> HCL parse error in line 2, column 1
+              |
+            2 | foo = 2
+              | ^---
+              |
+              = invalid attribute; expected unique attribute key; found redefined attribute"#}
+    );
 }
 
 #[test]