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

Issue:
Real multiple line problem
Hi,
But what about variables containing real newlines?
for example:

```
# .env.example
FOO=baz
MULTILINE_FOO='{
    "first": 1,
    "second": 1
  }'
BAR=baz
```
it is a real work example with `python-dotenv`
and dotenv-linter doesn't work with them:
```
dotenv-linter .env.example
Checking .env.example
.env.example:2 QuoteCharacter: The value has quote characters (', ")
.env.example:3 IncorrectDelimiter: The "first": 1, key has incorrect delimiter
.env.example:3 KeyWithoutValue: The "first": 1, key should be with a value or have an equal sign
.env.example:3 LeadingCharacter: Invalid leading character detected
.env.example:3 LowercaseKey: The "first": 1, key should be in uppercase
.env.example:3 UnorderedKey: The "first": 1, key should go before the FOO key
.env.example:4 IncorrectDelimiter: The "second": 1 key has incorrect delimiter
.env.example:4 KeyWithoutValue: The "second": 1 key should be with a value or have an equal sign
.env.example:4 LeadingCharacter: Invalid leading character detected
.env.example:4 LowercaseKey: The "second": 1 key should be in uppercase
.env.example:4 UnorderedKey: The "second": 1 key should go before the FOO key
.env.example:5 KeyWithoutValue: The }' key should be with a value or have an equal sign
.env.example:5 LeadingCharacter: Invalid leading character detected
.env.example:6 UnorderedKey: The BAR key should go before the FOO key

Found 14 problems
```



Pull request:
feat: add support for double quoted multi-line values
<!--
Thank you for your pull request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.
-->
Continuing to #450

Closes #442

It seems that we should update documentation. But tech-functionality is ready 🙂 


<!-- _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] Commit messages have been written in [Conventional Commits](https://www.conventionalcommits.org) format;
- [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);
- [ ] Docs have been added / updated on the [dotenv-linter.github.io](https://github.com/dotenv-linter/dotenv-linter.github.io) (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 9c5e31f..5ce084b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ # Changelog
 
 ## [Unreleased]
 ### 🚀 Added
+- Support of double-quoted multiline values [#453](https://github.com/dotenv-linter/dotenv-linter/pull/453) ([@DDtKey](https://github.com/DDtKey))
 - Support of single-quoted multiline values [#450](https://github.com/dotenv-linter/dotenv-linter/pull/450) ([@DDtKey](https://github.com/DDtKey))
 
 ### 🔧 Changed
diff --git a/src/common.rs b/src/common.rs
index 1ad6b56..88f3234 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -6,6 +6,7 @@
 pub(crate) mod comment;
 pub(crate) mod lint_kind;
 pub(crate) mod output;
+pub(crate) mod quote_type;
 
 pub use compare::CompareFileType;
 pub use compare::CompareWarning;
diff --git a/src/common/quote_type.rs b/src/common/quote_type.rs
new file mode 100644
index 0000000..2cd250b
--- /dev/null
+++ b/src/common/quote_type.rs
@@ -0,0 +1,67 @@
+use super::is_escaped;
+
+pub enum QuoteType {
+    Single,
+    Double,
+}
+
+impl QuoteType {
+    pub fn char(&self) -> char {
+        match self {
+            QuoteType::Single => '\'',
+            QuoteType::Double => '\"',
+        }
+    }
+
+    pub fn is_quoted_value(&self, val: &str) -> bool {
+        val.starts_with(self.char())
+            && (!val.ends_with(self.char()) || is_escaped(&val[..val.len() - 1]))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn check_single_quoted_value() {
+        assert_eq!(true, QuoteType::Single.is_quoted_value("\'some_quoted_str"))
+    }
+
+    #[test]
+    fn check_double_quoted_value() {
+        assert_eq!(true, QuoteType::Double.is_quoted_value("\"some_quoted_str"))
+    }
+
+    #[test]
+    fn check_non_single_quoted_value() {
+        assert_eq!(
+            false,
+            QuoteType::Single.is_quoted_value("some_non_quoted_str")
+        )
+    }
+
+    #[test]
+    fn check_non_double_quoted_value() {
+        assert_eq!(
+            false,
+            QuoteType::Double.is_quoted_value("some_non_quoted_str")
+        )
+    }
+
+    #[test]
+    fn check_single_quoted_value_for_double_quoted_str() {
+        assert_eq!(
+            false,
+            QuoteType::Single.is_quoted_value("\"some_double_quoted_str")
+        )
+    }
+
+    #[test]
+    fn check_double_quoted_value_for_single_quoted_str() {
+        assert_eq!(
+            false,
+            QuoteType::Double.is_quoted_value("\'some_single_quoted_str")
+        )
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 81392a7..fb31d38 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,6 +5,7 @@
 use crate::common::*;
 
 pub use checks::available_check_names;
+use common::quote_type::QuoteType;
 mod checks;
 mod common;
 mod fixes;
@@ -269,30 +270,25 @@ fn reduce_multiline_entries(lines: &mut Vec<LineEntry>) {
 fn find_multiline_ranges(lines: &[LineEntry]) -> Vec<(usize, usize)> {
     let mut multiline_ranges: Vec<(usize, usize)> = Vec::new();
     let mut start_number: Option<usize> = None;
+    let mut quote_char: Option<char> = None;
 
     // here we find ranges of multi-line values
     lines.iter().for_each(|entry| {
         if let Some(start) = start_number {
-            if let Some(idx) = entry.raw_string.find('\'') {
-                if !is_escaped(&entry.raw_string[..idx]) {
-                    multiline_ranges.push((start, entry.number));
+            if let Some(quote_char) = quote_char {
+                if let Some(idx) = entry.raw_string.find(quote_char) {
+                    if !is_escaped(&entry.raw_string[..idx]) {
+                        multiline_ranges.push((start, entry.number));
+                        start_number = None;
+                    }
+                } else if entry.get_value().is_some() {
+                    // if next line correct env line - then previous start-line incorrect multi-value
                     start_number = None;
                 }
-            } else if entry.get_value().is_some() {
-                // if next line correct env line - then previous start-line incorrect multi-value
-                start_number = None;
             }
-        } else {
-            let is_multiline_start = entry
-                .get_value()
-                .map(|val| val.trim())
-                .filter(|val| {
-                    val.starts_with('\'')
-                        && (!val.ends_with('\'') || is_escaped(&val[..val.len() - 1]))
-                })
-                .is_some();
-
-            if is_multiline_start {
+        } else if let Some(trimmed_value) = entry.get_value().map(|val| val.trim()) {
+            if let Some(quote_type) = is_multiline_start(trimmed_value) {
+                quote_char = Some(quote_type.char());
                 start_number = Some(entry.number);
             }
         }
@@ -300,3 +296,13 @@ fn find_multiline_ranges(lines: &[LineEntry]) -> Vec<(usize, usize)> {
 
     multiline_ranges
 }
+
+/// returns the QuoteType for a str starting with a quote-char
+fn is_multiline_start(val: &str) -> Option<QuoteType> {
+    for quote_type in [QuoteType::Single, QuoteType::Double] {
+        if quote_type.is_quoted_value(val) {
+            return Some(quote_type);
+        }
+    }
+    None
+}


Test patch:
diff --git a/tests/fixes/fixtures/complicated.env b/tests/fixes/fixtures/complicated.env
index c65927e..ffea37b 100644
--- a/tests/fixes/fixtures/complicated.env
+++ b/tests/fixes/fixtures/complicated.env
@@ -82,4 +82,11 @@ M_ENV1=   '{
   }'
 ABC=FOO
 M_ENV2 = 'multiline \'escaped\'
-    value'
\ No newline at end of file
+    value'
+M_ENV4="multiline \"escaped\"
+    value"
+M_ENV3=" #yml file
+  fields:
+    first: 1
+    second: 2
+"
\ No newline at end of file
diff --git a/tests/fixes/fixtures/complicated.env.golden b/tests/fixes/fixtures/complicated.env.golden
index a1eaade..d72fc48 100644
--- a/tests/fixes/fixtures/complicated.env.golden
+++ b/tests/fixes/fixtures/complicated.env.golden
@@ -80,4 +80,11 @@ M_ENV1='{
   }'
 M_ENV2='multiline \'escaped\'
     value'
+M_ENV3=" #yml file
+  fields:
+    first: 1
+    second: 2
+"
+M_ENV4="multiline \"escaped\"
+    value"
 ZOO=BAR
diff --git a/tests/fixes/fixtures/correct.env b/tests/fixes/fixtures/correct.env
index d3b9e7f..4f866de 100644
--- a/tests/fixes/fixtures/correct.env
+++ b/tests/fixes/fixtures/correct.env
@@ -17,8 +17,14 @@ A_ENV_13=D
 A_ENV_14=123
 A_ENV_15=345
 
-# multiline value
+# multiline values
 M_ENV1='{
     "first": 1,
     "second": 1
   }'
+M_ENV2="# yml file
+    fields: 
+      first: 1
+      second: 1
+"
+
diff --git a/tests/fixes/fixtures/correct.env.golden b/tests/fixes/fixtures/correct.env.golden
index d3b9e7f..4657372 100644
--- a/tests/fixes/fixtures/correct.env.golden
+++ b/tests/fixes/fixtures/correct.env.golden
@@ -17,8 +17,13 @@ A_ENV_13=D
 A_ENV_14=123
 A_ENV_15=345
 
-# multiline value
+# multiline values
 M_ENV1='{
     "first": 1,
     "second": 1
   }'
+M_ENV2="# yml file
+    fields: 
+      first: 1
+      second: 1
+"
diff --git a/tests/output/check.rs b/tests/output/check.rs
index c46ea1c..305946a 100644
--- a/tests/output/check.rs
+++ b/tests/output/check.rs
@@ -54,6 +54,22 @@ fn valid_multiline_value_test() {
     test_dir.test_command_success(expected_output);
 }
 
+#[test]
+fn valid_double_quoted_multiline_value_test() {
+    let test_dir = TestDir::new();
+    test_dir.create_testfile(
+        ".env",
+        "FOO=bar\nMULTILINE_1=\"#yml example \n first: 1 \n second: 1 \n\"\nMULTILINE_2=\"multiline \\\"escaped\\\" \n value\"\nZAC=baz\n",
+    );
+
+    let expected_output = r#"Checking .env
+
+No problems found
+"#;
+
+    test_dir.test_command_success(expected_output);
+}
+
 #[test]
 fn problems_first_and_last_file() {
     let test_dir = TestDir::new();