Repository: dotenv-linter/dotenv-linter (Rust)

Issue:
Add fixer: EndingBlankLineFixer
Implement `trait Fix` to fix warnings caused by `EndingBlankLineChecker`.
And add this `Fix` to the `fixlist` in [fixes.rs](https://github.com/dotenv-linter/dotenv-linter/blob/cf176993360215596c079873725f4a4b7a48f5b0/src/fixes.rs#L32)

Implementation example: [LowercaseKeyFixer](https://github.com/dotenv-linter/dotenv-linter/blob/master/src/fixes/lowercase_key.rs)



Discussion:
I was taking a look at this, and I'm running into an issue when implementing.

I was thinking something like this should work:
```
    fn fix_line(&self, line: &mut LineEntry) -> Option<()> {
        if line.is_last_line() && !line.raw_string.ends_with(LF) {
            line.raw_string.push_str("\n");
        }
        Some(())
    }
```
but what ends up happening is that if we have a file like, e.g.

```
ABC=DEF
FOO=BAR
THIS =THAT
```
The newline gets added but the THIS=THAT line gets completely deleted. I guess because of this line of code: https://github.com/dotenv-linter/dotenv-linter/blob/master/src/fs_utils.rs#L43, which isn't printing the final THIS=THAT line because it only goes to len()-1.

You can see the code I have here: https://github.com/baile320/dotenv-linter/tree/ending-blank-line-fixer and pull down to take a look at the behavior if you want, but I'm wondering if there's a simpler way to solve the issue apart from messing with the existing fs_utils (maybe you know off the top of your head, I'm just poking around :))

Hi! 
I think, I can help, but I'm little busy right now, I will write you later (maybe tomorrow).
Thank you! No hurry... I can probably find a few solutions, but I wouldn't be sure if there is a preferred one, so I appreciate the help 👍

As an example fix, we could do something like this (I have this code pushed to my branch/fork linked above), and it looks like it does not break any existing tests.:

```rust
/// In the future versions we should create a backup copy, or at least notify the user about it
pub fn write_file(path: &PathBuf, lines: Vec<LineEntry>) -> io::Result<()> {
    let mut file = File::create(path)?;

    // We write all but the last line using writeln! to insert LFs
    for line in lines[..lines.len() - 1].iter() {
        writeln!(file, "{}", line.raw_string)?;
    }

    // For the last line, if there is actual content, we use write! because EndingBlankLineFixer 
    // can append LFs to the final line, and if we don't write it then it gets deleted
    if let Some(line) = lines.last() {
        if line.raw_string != LF && line.raw_string.ends_with(LF) {
            write!(file, "{}", line.raw_string)?;
        }
    }


    Ok(())
}
```
Hello again!

When `Fix` trait was implemented (https://github.com/dotenv-linter/dotenv-linter/pull/228) I assumed that there are two groups of fixes: some can be implemented by working only with the line that caused warning, for others you need to work with the entire line collection.

I think that this fixer belongs to the secong group. So you can implement the `fix_warnings` method instead of the `fix_line` method and just append a blank line to the line collection.

This was the original idea. But maybe there is a better solution, if you find it, it will be interesting to discuss it.






We are modeling an ending blank line as a separate line entry: 
https://github.com/dotenv-linter/dotenv-linter/blob/612e1a96a0f1645dab1c9b16a2f0eddb2a9046a7/src/common.rs#L79

So I think it would be consistent to fix this warning by adding a separate line entry.


No, I think that makes sense and is much easier than what I was trying. Thanks for pointing me in the right direction!



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e75b67120..81e23d2b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 ### 🚀 Added
+- Add autofix for EndingBlankLine [#263](https://github.com/dotenv-linter/dotenv-linter/pull/263) ([@baile320](https://github.com/baile320))
 - Add test for two keys that only differ in case [#269](https://github.com/dotenv-linter/dotenv-linter/pull/269) ([@yanakad](https://github.com/yanakad))
 - Add autofix for TrailingWhitespace [#255](https://github.com/dotenv-linter/dotenv-linter/pull/255) ([@gregcline](https://github.com/gregcline))
 - Add fixer: KeyWithoutValueFixer [#254](https://github.com/dotenv-linter/dotenv-linter/pull/254) ([@unexge](https://github.com/unexge))
diff --git a/src/fixes.rs b/src/fixes.rs
index 473368355..b324fddfc 100644
--- a/src/fixes.rs
+++ b/src/fixes.rs
@@ -1,5 +1,6 @@
 use crate::common::*;
 
+mod ending_blank_line;
 mod key_without_value;
 mod lowercase_key;
 mod quote_character;
@@ -45,6 +46,7 @@ fn fixlist() -> Vec<Box<dyn Fix>> {
         // Then we should run the fixers that handle the line entry collection at whole.
         // And at the end we should run the fixer for ExtraBlankLine check (because the previous
         // fixers can create additional extra blank lines).
+        Box::new(ending_blank_line::EndingBlankLineFixer::default()),
     ]
 }
 
diff --git a/src/fixes/ending_blank_line.rs b/src/fixes/ending_blank_line.rs
new file mode 100644
index 000000000..fc31e603f
--- /dev/null
+++ b/src/fixes/ending_blank_line.rs
@@ -0,0 +1,113 @@
+use super::Fix;
+use crate::common::*;
+
+pub(crate) struct EndingBlankLineFixer<'a> {
+    name: &'a str,
+}
+
+impl Default for EndingBlankLineFixer<'_> {
+    fn default() -> Self {
+        Self {
+            name: "EndingBlankLine",
+        }
+    }
+}
+
+impl Fix for EndingBlankLineFixer<'_> {
+    fn name(&self) -> &str {
+        self.name
+    }
+
+    fn fix_warnings(
+        &self,
+        warnings: Vec<&mut Warning>,
+        lines: &mut Vec<LineEntry>,
+    ) -> Option<usize> {
+        let file = lines.first()?.file.clone();
+        let last_line = lines.last()?;
+
+        if !last_line.raw_string.ends_with(LF) {
+            lines.push(LineEntry {
+                number: lines.len() + 1,
+                file,
+                raw_string: LF.to_string(),
+            });
+
+            for warning in warnings {
+                warning.mark_as_fixed()
+            }
+
+            return Some(1);
+        }
+
+        Some(0)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::path::PathBuf;
+
+    #[test]
+    fn fix_warnings_test() {
+        let fixer = EndingBlankLineFixer::default();
+        let mut lines = vec![
+            LineEntry {
+                number: 1,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 2,
+                },
+                raw_string: String::from("FOO=BAR"),
+            },
+            LineEntry {
+                number: 2,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 2,
+                },
+                raw_string: String::from("Z=Y"),
+            },
+        ];
+        let mut warning = Warning::new(
+            lines[1].clone(),
+            "EndingBlankLine",
+            String::from("No blank line at the end of the file"),
+        );
+
+        assert_eq!(Some(1), fixer.fix_warnings(vec![&mut warning], &mut lines));
+        assert_eq!("\n", lines[2].raw_string);
+        assert!(warning.is_fixed);
+    }
+
+    #[test]
+    fn ending_blank_line_exist_test() {
+        let fixer = EndingBlankLineFixer::default();
+        let mut lines = vec![
+            LineEntry {
+                number: 1,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 2,
+                },
+                raw_string: String::from("FOO=BAR"),
+            },
+            LineEntry {
+                number: 2,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 2,
+                },
+                raw_string: String::from(LF),
+            },
+        ];
+
+        assert_eq!(Some(0), fixer.fix_warnings(vec![], &mut lines));
+        assert_eq!(lines.len(), 2);
+    }
+}


Test patch:
diff --git a/tests/fixes/ending_blank_line.rs b/tests/fixes/ending_blank_line.rs
new file mode 100644
index 000000000..f33efec9b
--- /dev/null
+++ b/tests/fixes/ending_blank_line.rs
@@ -0,0 +1,16 @@
+use crate::common::TestDir;
+
+#[test]
+fn ending_blank_line() {
+    let testdir = TestDir::new();
+    let testfile = testdir.create_testfile(".env", "ABC=DEF\nFOO=BAR");
+    let expected_output = String::from(
+        "Fixed warnings:\n\
+        .env:2 EndingBlankLine: No blank line at the end of the file\n",
+    );
+    testdir.test_command_fix_success(expected_output);
+
+    assert_eq!(testfile.contents().as_str(), "ABC=DEF\nFOO=BAR\n");
+
+    testdir.close();
+}
diff --git a/tests/fixes/mod.rs b/tests/fixes/mod.rs
index 31e4ac7f0..99ff55a85 100644
--- a/tests/fixes/mod.rs
+++ b/tests/fixes/mod.rs
@@ -2,6 +2,7 @@ mod quote_character;
 
 use crate::common::TestDir;
 
+mod ending_blank_line;
 mod space_character;
 mod trailing_whitespace;