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

Issue:
Add fixer: LeadingCharacterFixer
Implement `trait Fix` to fix warnings caused by `LeadingCharacterChecker`.
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
*RAILS_ENV=development

# Correct (After running `LeadingCharacterFixer` it should look like this)
RAILS_ENV=development
```


Implementation example: [LowercaseKeyFixer](https://github.com/dotenv-linter/dotenv-linter/blob/master/src/fixes/lowercase_key.rs)


Discussion:
I would like to take this one.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ce2140620..4199fe0ce 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 fixer: LeadingCharacter [#259](https://github.com/dotenv-linter/dotenv-linter/pull/259) ([@baile320](https://github.com/baile320))
 - Add fixer: IncorrectDelimiter [#258]((https://github.com/dotenv-linter/dotenv-linter/pull/258) ([@gillespiecd](https://github.com/gillespiecd))
 - Add support of comments to disable checks [#239](https://github.com/dotenv-linter/dotenv-linter/pull/239) ([@mgrachev](https://github.com/mgrachev))
 - Add autofix for EndingBlankLine [#263](https://github.com/dotenv-linter/dotenv-linter/pull/263) ([@baile320](https://github.com/baile320))
diff --git a/src/checks/leading_character.rs b/src/checks/leading_character.rs
index 246fe96f5..a1b273063 100644
--- a/src/checks/leading_character.rs
+++ b/src/checks/leading_character.rs
@@ -47,7 +47,7 @@ mod tests {
     const MESSAGE: &str = "Invalid leading character detected";
 
     #[test]
-    fn normal() {
+    fn no_leading_chars_test() {
         let mut checker = LeadingCharacterChecker::default();
         let line = LineEntry {
             number: 1,
diff --git a/src/fixes.rs b/src/fixes.rs
index e05a79ca8..30501bb7e 100644
--- a/src/fixes.rs
+++ b/src/fixes.rs
@@ -3,6 +3,7 @@ use crate::common::*;
 mod ending_blank_line;
 mod incorrect_delimiter;
 mod key_without_value;
+mod leading_character;
 mod lowercase_key;
 mod quote_character;
 mod space_character;
@@ -42,6 +43,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(leading_character::LeadingCharacterFixer::default()),
         Box::new(quote_character::QuoteCharacterFixer::default()),
         Box::new(incorrect_delimiter::IncorrectDelimiterFixer::default()),
         // Then we should run the fixers that handle the line entry collection at whole.
diff --git a/src/fixes/leading_character.rs b/src/fixes/leading_character.rs
new file mode 100644
index 000000000..15416e93e
--- /dev/null
+++ b/src/fixes/leading_character.rs
@@ -0,0 +1,267 @@
+use super::Fix;
+use crate::common::*;
+
+pub(crate) struct LeadingCharacterFixer<'a> {
+    name: &'a str,
+}
+
+impl Default for LeadingCharacterFixer<'_> {
+    fn default() -> Self {
+        Self {
+            name: "LeadingCharacter",
+        }
+    }
+}
+
+impl Fix for LeadingCharacterFixer<'_> {
+    fn name(&self) -> &str {
+        self.name
+    }
+
+    fn fix_line(&self, line: &mut LineEntry) -> Option<()> {
+        let key = line.get_key()?;
+
+        let cleaned_key = remove_invalid_leading_chars(&key);
+
+        line.raw_string = format!("{}={}", cleaned_key, line.get_value()?);
+
+        Some(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::path::PathBuf;
+
+    #[test]
+    fn fix_leading_dot() {
+        let fixer = LeadingCharacterFixer::default();
+
+        let mut leading_period = LineEntry {
+            number: 2,
+            file: FileEntry {
+                path: PathBuf::from(".env"),
+                file_name: ".env".to_string(),
+                total_lines: 7,
+            },
+            raw_string: String::from(".FOO=BAR"),
+        };
+
+        assert_eq!(Some(()), fixer.fix_line(&mut leading_period));
+        assert_eq!("FOO=BAR", leading_period.raw_string);
+    }
+
+    #[test]
+    fn fix_leading_space() {
+        let fixer = LeadingCharacterFixer::default();
+
+        let mut leading_space = LineEntry {
+            number: 1,
+            file: FileEntry {
+                path: PathBuf::from(".env"),
+                file_name: ".env".to_string(),
+                total_lines: 7,
+            },
+            raw_string: String::from(" FOO=BAR"),
+        };
+
+        assert_eq!(Some(()), fixer.fix_line(&mut leading_space));
+        assert_eq!("FOO=BAR", leading_space.raw_string);
+    }
+
+    #[test]
+    fn fix_leading_asterisk() {
+        let fixer = LeadingCharacterFixer::default();
+
+        let mut leading_asterisk = LineEntry {
+            number: 3,
+            file: FileEntry {
+                path: PathBuf::from(".env"),
+                file_name: ".env".to_string(),
+                total_lines: 7,
+            },
+            raw_string: String::from("*FOO=BAR"),
+        };
+
+        assert_eq!(Some(()), fixer.fix_line(&mut leading_asterisk));
+        assert_eq!("FOO=BAR", leading_asterisk.raw_string);
+    }
+
+    #[test]
+    fn fix_leading_number() {
+        let fixer = LeadingCharacterFixer::default();
+
+        let mut leading_number = LineEntry {
+            number: 4,
+            file: FileEntry {
+                path: PathBuf::from(".env"),
+                file_name: ".env".to_string(),
+                total_lines: 7,
+            },
+            raw_string: String::from("1FOO=BAR"),
+        };
+
+        assert_eq!(Some(()), fixer.fix_line(&mut leading_number));
+        assert_eq!("FOO=BAR", leading_number.raw_string);
+    }
+
+    #[test]
+    fn fix_many_invalid_leading_chars() {
+        let fixer = LeadingCharacterFixer::default();
+
+        let mut leading_number = LineEntry {
+            number: 4,
+            file: FileEntry {
+                path: PathBuf::from(".env"),
+                file_name: ".env".to_string(),
+                total_lines: 7,
+            },
+            raw_string: String::from("-1&*FOO=BAR"),
+        };
+
+        assert_eq!(Some(()), fixer.fix_line(&mut leading_number));
+        assert_eq!("FOO=BAR", leading_number.raw_string);
+    }
+
+    #[test]
+    fn leading_underscore_is_unchanged() {
+        let fixer = LeadingCharacterFixer::default();
+        let mut leading_underscore = LineEntry {
+            number: 5,
+            file: FileEntry {
+                path: PathBuf::from(".env"),
+                file_name: ".env".to_string(),
+                total_lines: 7,
+            },
+            raw_string: String::from("_FOO=BAR"),
+        };
+
+        assert_eq!(Some(()), fixer.fix_line(&mut leading_underscore));
+        assert_eq!("_FOO=BAR", leading_underscore.raw_string);
+    }
+
+    #[test]
+    fn no_leading_char_is_unchanged() {
+        let fixer = LeadingCharacterFixer::default();
+
+        let mut normal = LineEntry {
+            number: 6,
+            file: FileEntry {
+                path: PathBuf::from(".env"),
+                file_name: ".env".to_string(),
+                total_lines: 7,
+            },
+            raw_string: String::from("FOO=BAR"),
+        };
+
+        assert_eq!(Some(()), fixer.fix_line(&mut normal));
+        assert_eq!("FOO=BAR", normal.raw_string);
+    }
+
+    #[test]
+    fn fix_warnings_test() {
+        let fixer = LeadingCharacterFixer::default();
+        let mut lines = vec![
+            LineEntry {
+                number: 1,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 7,
+                },
+                raw_string: String::from(".FOO=BAR"),
+            },
+            LineEntry {
+                number: 2,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 7,
+                },
+                raw_string: String::from(" Z=Y"),
+            },
+            LineEntry {
+                number: 3,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 7,
+                },
+                raw_string: String::from("*BAR=BAZ"),
+            },
+            LineEntry {
+                number: 4,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 7,
+                },
+                raw_string: String::from("1QUX=QUUX"),
+            },
+            LineEntry {
+                number: 5,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 7,
+                },
+                raw_string: String::from("_QUUX=FOOBAR"),
+            },
+            LineEntry {
+                number: 6,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 7,
+                },
+                raw_string: String::from("KEY=VALUE"),
+            },
+            LineEntry {
+                number: 7,
+                file: FileEntry {
+                    path: PathBuf::from(".env"),
+                    file_name: ".env".to_string(),
+                    total_lines: 7,
+                },
+                raw_string: String::from("\n"),
+            },
+        ];
+
+        let mut warnings = vec![
+            Warning::new(
+                lines[0].clone(),
+                "LeadingCharacter",
+                String::from("Invalid leading character detected"),
+            ),
+            Warning::new(
+                lines[1].clone(),
+                "LeadingCharacter",
+                String::from("Invalid leading character detected"),
+            ),
+            Warning::new(
+                lines[2].clone(),
+                "LeadingCharacter",
+                String::from("Invalid leading character detected"),
+            ),
+            Warning::new(
+                lines[3].clone(),
+                "LeadingCharacter",
+                String::from("Invalid leading character detected"),
+            ),
+        ];
+
+        assert_eq!(
+            Some(4),
+            fixer.fix_warnings(warnings.iter_mut().collect(), &mut lines)
+        );
+
+        assert_eq!("FOO=BAR", lines[0].raw_string);
+        assert_eq!("Z=Y", lines[1].raw_string);
+        assert_eq!("BAR=BAZ", lines[2].raw_string);
+        assert_eq!("QUX=QUUX", lines[3].raw_string);
+        assert_eq!("_QUUX=FOOBAR", lines[4].raw_string);
+        assert_eq!("KEY=VALUE", lines[5].raw_string);
+        assert_eq!("\n", lines[6].raw_string);
+    }
+}


Test patch:
diff --git a/tests/fixes/leading_character.rs b/tests/fixes/leading_character.rs
new file mode 100644
index 000000000..c8ab086af
--- /dev/null
+++ b/tests/fixes/leading_character.rs
@@ -0,0 +1,22 @@
+use crate::common::TestDir;
+
+#[test]
+fn leading_character() {
+    let testdir = TestDir::new();
+    let testfile = testdir.create_testfile(".env", "*BAR=BAZ\n.ABC=DEF\n1QUX=QUUX\n_FOO=BAR\n");
+    let expected_output = String::from(
+        "Fixed warnings:\n\
+        .env:1 LeadingCharacter: Invalid leading character detected\n\
+        .env:2 LeadingCharacter: Invalid leading character detected\n\
+        .env:3 LeadingCharacter: Invalid leading character detected\n",
+    );
+
+    testdir.test_command_fix_success(expected_output);
+
+    assert_eq!(
+        testfile.contents().as_str(),
+        "BAR=BAZ\nABC=DEF\nQUX=QUUX\n_FOO=BAR\n"
+    );
+
+    testdir.close();
+}
diff --git a/tests/fixes/mod.rs b/tests/fixes/mod.rs
index 58bbd6cb5..0fc40e8ee 100644
--- a/tests/fixes/mod.rs
+++ b/tests/fixes/mod.rs
@@ -4,6 +4,7 @@ use crate::common::TestDir;
 
 mod ending_blank_line;
 mod incorrect_delimiter;
+mod leading_character;
 mod space_character;
 mod trailing_whitespace;