Skip to content

Commit

Permalink
fix(structure): handle absence of trailing newline in body (#273)
Browse files Browse the repository at this point in the history
Fixes #270
  • Loading branch information
martinohmann authored Jul 14, 2023
1 parent d211041 commit 2f54cb1
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 9 deletions.
13 changes: 10 additions & 3 deletions crates/hcl-edit/src/encode/structure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ use std::fmt::{self, Write};

impl Encode for Body {
fn encode(&self, buf: &mut EncodeState) -> fmt::Result {
for structure in self.iter() {
for (index, structure) in self.iter().enumerate() {
structure.encode_decorated(buf, NO_DECOR)?;
buf.write_char('\n')?;

if index < self.len() - 1 || !self.prefer_omit_trailing_newline() {
buf.write_char('\n')?;
}
}

Ok(())
Expand Down Expand Up @@ -52,7 +55,11 @@ impl Encode for Block {
}
} else {
buf.write_char('\n')?;
body.encode(buf)?;

for structure in body {
structure.encode_decorated(buf, NO_DECOR)?;
buf.write_char('\n')?;
}
}

Ok(())
Expand Down
10 changes: 9 additions & 1 deletion crates/hcl-edit/src/parser/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub(super) struct BodyParseState<'a> {
current: Option<Structure>,
structures: Vec<Structure>,
ws: Option<Range<usize>>,
eof: bool,
}

impl<'a> BodyParseState<'a> {
Expand Down Expand Up @@ -48,8 +49,15 @@ impl<'a> BodyParseState<'a> {
self.structures.push(current);
}

pub(super) fn on_eof(&mut self) {
self.on_line_ending();
self.eof = true;
}

pub(super) fn into_body(self) -> Body {
Body::from_vec_unchecked(self.structures)
let mut body = Body::from_vec_unchecked(self.structures);
body.set_prefer_omit_trailing_newline(self.eof);
body
}
}

Expand Down
13 changes: 8 additions & 5 deletions crates/hcl-edit/src/parser/structure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ pub(super) fn body(input: &mut Input) -> PResult<Body> {
.span()
.map(|span| state.borrow_mut().on_ws(span)),
),
cut_err(alt((line_ending, eof)).map(|_| state.borrow_mut().on_line_ending()))
.context(StrContext::Expected(StrContextValue::Description(
"newline",
)))
.context(StrContext::Expected(StrContextValue::Description("eof"))),
cut_err(alt((
line_ending.map(|_| state.borrow_mut().on_line_ending()),
eof.map(|_| state.borrow_mut().on_eof()),
)))
.context(StrContext::Expected(StrContextValue::Description(
"newline",
)))
.context(StrContext::Expected(StrContextValue::Description("eof"))),
),
))
.span(),
Expand Down
1 change: 1 addition & 0 deletions crates/hcl-edit/src/parser/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ fn roundtrip_body() {
"#},
"block { attr = 1 }\n",
"foo = \"bar\"\nbar = 2\n",
"foo = \"bar\"\nbar = 3",
indoc! {r#"
indented_heredoc = <<-EOT
${foo}
Expand Down
22 changes: 22 additions & 0 deletions crates/hcl-edit/src/structure/body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ pub type BlocksMut<'a> = Box<dyn Iterator<Item = &'a mut Block> + 'a>;
pub struct Body {
structures: Vec<Structure>,
prefer_oneline: bool,
prefer_omit_trailing_newline: bool,
decor: Decor,
span: Option<Range<usize>>,
}
Expand Down Expand Up @@ -644,6 +645,27 @@ impl Body {
self.prefer_oneline
}

/// Configures whether the trailing newline after the last structure in the body should be
/// omitted.
///
/// This is only a hint which will be applied if this is the top-level `Body` of a HCL
/// document and is ignored if the `Body` is part of a [`Block`].
///
/// The default is to always emit a trailing newline after the last body structure.
#[inline]
pub fn set_prefer_omit_trailing_newline(&mut self, yes: bool) {
self.prefer_omit_trailing_newline = yes;
}

/// Returns `true` if the trailing newline after the last structure in the body should be
/// omitted.
///
/// See the documentation of [`Body::set_prefer_omit_trailing_newline`] for more.
#[inline]
pub fn prefer_omit_trailing_newline(&self) -> bool {
self.prefer_omit_trailing_newline
}

/// Returns `true` if the body only consist of a single `Attribute`.
#[inline]
pub(crate) fn has_single_attribute(&self) -> bool {
Expand Down
29 changes: 29 additions & 0 deletions crates/hcl-edit/tests/regressions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use hcl_edit::expr::Expression;
use hcl_edit::structure::{Attribute, Block, Body};
use hcl_edit::template::{Element, Interpolation, Template};
use hcl_edit::Ident;
use pretty_assertions::assert_eq;
Expand Down Expand Up @@ -28,3 +29,31 @@ fn issue_256() {

assert_eq!(parsed, expected);
}

// https://github.com/martinohmann/hcl-rs/issues/270
#[test]
fn issue_270() {
let no_trailing_newline = String::from("block {\nfoo = \"bar\"\n}\nbar = \"baz\"");
let trailing_newline = format!("{no_trailing_newline}\n");

// Parsed
let parsed: Body = no_trailing_newline.parse().unwrap();
assert_eq!(parsed.to_string(), no_trailing_newline);

let parsed: Body = trailing_newline.parse().unwrap();
assert_eq!(parsed.to_string(), trailing_newline);

// Manually constructed
let mut body = Body::builder()
.block(
Block::builder(Ident::new("block"))
.attribute(Attribute::new(Ident::new("foo"), "bar"))
.build(),
)
.attribute(Attribute::new(Ident::new("bar"), "baz"))
.build();
assert_eq!(body.to_string(), trailing_newline);

body.set_prefer_omit_trailing_newline(true);
assert_eq!(body.to_string(), no_trailing_newline);
}

0 comments on commit 2f54cb1

Please sign in to comment.