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

Issue:
Add fixer: QuoteCharacterFixer
Implement `trait Fix` to fix warnings caused by `QuoteCharacterChecker`.
And add this `Fix` to the `fixlist` in [fixes.rs](https://github.com/dotenv-linter/dotenv-linter/blob/cf176993360215596c079873725f4a4b7a48f5b0/src/fixes.rs#L32)

```env
# .env

# Wrong
FOO="BAR"

# Correct (After running `QuoteCharacterFixer` it should look like this)
FOO=BAR
```

Implementation example: [LowercaseKeyFixer](https://github.com/dotenv-linter/dotenv-linter/blob/master/src/fixes/lowercase_key.rs)

Pull request description:
Implement trait `Fix` for warnings issued by `QuoteCharacterChecker`.
Closes #249.

<!--
Thank you for your pull request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.
-->

<!-- _Please make sure to review and check all of these items:_ -->

#### ✔ Checklist:
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] This PR has been added to [CHANGELOG.md](https://github.com/dotenv-linter/dotenv-linter/blob/master/CHANGELOG.md) (at the top of the list);
- [X] Tests for the changes have been added (for bug fixes / features);

<!-- _NOTE: these things are not required to open a PR and can be done afterwards / while the PR is open._ -->


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 96d5a9a..427b425 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ ## [Unreleased]
 ### 🚀 Added
 - 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))
+- Add fixer: QuoteCharacterFixer [#257](https://github.com/dotenv-linter/dotenv-linter/pull/257) ([@lensvol](https://github.com/lensvol))
 - Add fixer: SpaceCharacterFixer [#253](https://github.com/dotenv-linter/dotenv-linter/pull/253) ([@DDtKey](https://github.com/DDtKey))
 - Add total problems to output and `--quiet` argument [#242](https://github.com/dotenv-linter/dotenv-linter/pull/242) ([@wesleimp](https://github.com/wesleimp), [@mgrachev](https://github.com/mgrachev))
 - Add autofix feature (for LowercaseKey check) [#228](https://github.com/dotenv-linter/dotenv-linter/pull/228) ([@evgeniy-r](https://github.com/evgeniy-r))
diff --git a/README.md b/README.md
index 90a6c4a..34b257b 100644
--- a/README.md
+++ b/README.md
@@ -372,7 +372,7 @@ ### Lowercase key
 
 ### Quote character
 
-Detects if a value is wrapped in quotes:
+Detects if a value contains quote characters (`'` / `"`):
 
 ```env
 ❌ Wrong
@@ -381,6 +381,9 @@ ### Quote character
 ❌ Wrong
 FOO='BAR'
 
+❌ Wrong
+FOO='B"AR'
+
 ✅ Correct
 FOO=BAR
 ```
diff --git a/docs/checks/quote_character.md b/docs/checks/quote_character.md
index e665b70..f0af77f 100644
--- a/docs/checks/quote_character.md
+++ b/docs/checks/quote_character.md
@@ -1,6 +1,6 @@
 # Quote character
 
-Detects if a value is wrapped in quotes:
+Detects if a value contains quote characters (`'` / `"`):
 
 ```env
 ❌ Wrong
@@ -9,6 +9,9 @@ # Quote character
 ❌ Wrong
 FOO='BAR'
 
+❌ Wrong
+FOO='B"AR'
+
 ✅ Correct
 FOO=BAR
 ```
diff --git a/src/checks/quote_character.rs b/src/checks/quote_character.rs
index 503ef03..f366ed7 100644
--- a/src/checks/quote_character.rs
+++ b/src/checks/quote_character.rs
@@ -16,7 +16,7 @@ impl Default for QuoteCharacterChecker<'_> {
     fn default() -> Self {
         Self {
             name: "QuoteCharacter",
-            template: "The value is wrapped in quotes",
+            template: "The value has quote characters (\', \")",
         }
     }
 }
@@ -86,7 +86,31 @@ fn with_single_quote_test() {
                         raw_string: String::from("FOO='BAR'"),
                     },
                     "QuoteCharacter",
-                    String::from("The value is wrapped in quotes"),
+                    String::from("The value has quote characters (\', \")"),
+                )),
+            ),
+            (
+                LineEntry {
+                    number: 2,
+                    file: FileEntry {
+                        path: PathBuf::from(".env"),
+                        file_name: ".env".to_string(),
+                        total_lines: 2,
+                    },
+                    raw_string: String::from("FOO='B\"AR'"),
+                },
+                Some(Warning::new(
+                    LineEntry {
+                        number: 2,
+                        file: FileEntry {
+                            path: PathBuf::from(".env"),
+                            file_name: ".env".to_string(),
+                            total_lines: 2,
+                        },
+                        raw_string: String::from("FOO='B\"AR'"),
+                    },
+                    "QuoteCharacter",
+                    String::from("The value has quote characters (\', \")"),
                 )),
             ),
         ];
@@ -130,7 +154,7 @@ fn with_double_quote_test() {
                         raw_string: String::from("FOO=\"BAR\""),
                     },
                     "QuoteCharacter",
-                    String::from("The value is wrapped in quotes"),
+                    String::from("The value has quote characters (\', \")"),
                 )),
             ),
         ];
diff --git a/src/fixes.rs b/src/fixes.rs
index edb5ecf..264374f 100644
--- a/src/fixes.rs
+++ b/src/fixes.rs
@@ -2,6 +2,7 @@
 
 mod key_without_value;
 mod lowercase_key;
+mod quote_character;
 mod space_character;
 mod trailing_whitespace;
 
@@ -40,6 +41,7 @@ fn fixlist() -> Vec<Box<dyn Fix>> {
         Box::new(lowercase_key::LowercaseKeyFixer::default()),
         Box::new(space_character::SpaceCharacterFixer::default()),
         Box::new(trailing_whitespace::TrailingWhitespaceFixer::default()),
+        Box::new(quote_character::QuoteCharacterFixer::default()),
         // 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).
diff --git a/src/fixes/quote_character.rs b/src/fixes/quote_character.rs
new file mode 100644
index 0000000..07c19aa
--- /dev/null
+++ b/src/fixes/quote_character.rs
@@ -0,0 +1,94 @@
+use super::Fix;
+use crate::common::*;
+
+pub(crate) struct QuoteCharacterFixer<'a> {
+    name: &'a str,
+}
+
+impl Default for QuoteCharacterFixer<'_> {
+    fn default() -> Self {
+        Self {
+            name: "QuoteCharacter",
+        }
+    }
+}
+
+impl Fix for QuoteCharacterFixer<'_> {
+    fn name(&self) -> &str {
+        self.name
+    }
+
+    fn fix_line(&self, line: &mut LineEntry) -> Option<()> {
+        let value = line.get_value()?;
+        let pure_val = value.replace("'", "").replace("\"", "");
+
+        line.raw_string = format!("{}={}", line.get_key()?, pure_val);
+
+        Some(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::path::PathBuf;
+
+    #[test]
+    fn fix_line_test() {
+        let fixer = QuoteCharacterFixer::default();
+        let mut line = LineEntry {
+            number: 1,
+            file: FileEntry {
+                path: PathBuf::from(".env"),
+                file_name: ".env".to_string(),
+                total_lines: 1,
+            },
+            raw_string: String::from("FOO=\'\"b\'\"ar\"\'"),
+        };
+        assert_eq!(Some(()), fixer.fix_line(&mut line));
+        assert_eq!("FOO=bar", line.raw_string);
+    }
+
+    #[test]
+    fn fix_warnings_test() {
+        let fixer = QuoteCharacterFixer::default();
+        let mut lines = vec![
+            LineEntry {
+                number: 1,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 3,
+                },
+                raw_string: String::from("FOO=\"bar\'\""),
+            },
+            LineEntry {
+                number: 2,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 3,
+                },
+                raw_string: String::from("Z=Y"),
+            },
+            LineEntry {
+                number: 3,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 3,
+                },
+                raw_string: String::from("\n"),
+            },
+        ];
+        let mut warning = Warning::new(
+            lines[0].clone(),
+            "QuoteCharacter",
+            String::from("The value has quote characters (\', \")"),
+        );
+
+        assert_eq!(Some(1), fixer.fix_warnings(vec![&mut warning], &mut lines));
+        assert_eq!("FOO=bar", lines[0].raw_string);
+        assert!(warning.is_fixed);
+    }
+}


Test patch:
diff --git a/tests/fixes/mod.rs b/tests/fixes/mod.rs
index 0cdff80..31e4ac7 100644
--- a/tests/fixes/mod.rs
+++ b/tests/fixes/mod.rs
@@ -1,3 +1,5 @@
+mod quote_character;
+
 use crate::common::TestDir;
 
 mod space_character;
diff --git a/tests/fixes/quote_character.rs b/tests/fixes/quote_character.rs
new file mode 100644
index 0000000..c6c972b
--- /dev/null
+++ b/tests/fixes/quote_character.rs
@@ -0,0 +1,17 @@
+use crate::common::TestDir;
+
+#[test]
+fn quote_character() {
+    let testdir = TestDir::new();
+    let testfile = testdir.create_testfile(".env", "ABC=\"DEF\"\n\nFOO=\'B\"AR\'\n");
+    let expected_output = String::from(
+        "Fixed warnings:\n\
+        .env:1 QuoteCharacter: The value has quote characters (\', \")\n\
+        .env:3 QuoteCharacter: The value has quote characters (\', \")\n",
+    );
+    testdir.test_command_fix_success(expected_output);
+
+    assert_eq!(testfile.contents().as_str(), "ABC=DEF\n\nFOO=BAR\n");
+
+    testdir.close();
+}
diff --git a/tests/flags/quiet.rs b/tests/flags/quiet.rs
index 02dcfe3..a4c234e 100644
--- a/tests/flags/quiet.rs
+++ b/tests/flags/quiet.rs
@@ -7,7 +7,7 @@ fn output_without_total() {
 
     let args = &["--quiet"];
     let expected_output = format!(
-        "{a}:1 LeadingCharacter: Invalid leading character detected\n{a}:1 QuoteCharacter: The value is wrapped in quotes\n",
+        "{a}:1 LeadingCharacter: Invalid leading character detected\n{a}:1 QuoteCharacter: The value has quote characters (\', \")\n",
         a=testfile_to_check.shortname_as_str()
     );