dotenv_analyzer/check/
mod.rs

1use dotenv_core::LineEntry;
2use dotenv_schema::DotEnvSchema;
3
4use crate::{Comment, LintKind, Warning};
5
6mod duplicated_key;
7mod ending_blank_line;
8mod extra_blank_line;
9mod incorrect_delimiter;
10mod key_without_value;
11mod leading_character;
12mod lowercase_key;
13mod quote_character;
14mod schema_violation;
15mod space_character;
16mod substitution_key;
17mod trailing_whitespace;
18mod unordered_key;
19mod value_without_quotes;
20
21// This trait is used for checks which needs to know of only a single line
22pub trait Check {
23    fn run(&mut self, line: &LineEntry) -> Option<Warning>;
24    fn name(&self) -> LintKind;
25    fn skip_comments(&self) -> bool {
26        true
27    }
28    fn end(&mut self) -> Vec<Warning> {
29        vec![]
30    }
31}
32
33// Checklist for checks which needs to know of only a single line
34fn checklist<'a>(schema: Option<&'a DotEnvSchema>) -> Vec<Box<dyn Check + 'a>> {
35    vec![
36        Box::<duplicated_key::DuplicatedKeyChecker>::default(),
37        Box::<ending_blank_line::EndingBlankLineChecker>::default(),
38        Box::<extra_blank_line::ExtraBlankLineChecker>::default(),
39        Box::<incorrect_delimiter::IncorrectDelimiterChecker>::default(),
40        Box::<key_without_value::KeyWithoutValueChecker>::default(),
41        Box::<leading_character::LeadingCharacterChecker>::default(),
42        Box::<lowercase_key::LowercaseKeyChecker>::default(),
43        Box::<quote_character::QuoteCharacterChecker>::default(),
44        Box::<space_character::SpaceCharacterChecker>::default(),
45        Box::<substitution_key::SubstitutionKeyChecker>::default(),
46        Box::<trailing_whitespace::TrailingWhitespaceChecker>::default(),
47        Box::<unordered_key::UnorderedKeyChecker>::default(),
48        Box::<value_without_quotes::ValueWithoutQuotesChecker>::default(),
49        Box::new(schema_violation::SchemaViolationChecker::new(schema)),
50    ]
51}
52
53pub fn check(
54    lines: &[LineEntry],
55    skip_checks: &[LintKind],
56    schema: Option<&DotEnvSchema>,
57) -> Vec<Warning> {
58    let mut checks = checklist(schema);
59
60    // Skip checks with the --skip argument (globally)
61    checks.retain(|c| !skip_checks.contains(&c.name()));
62
63    // Skip checks with comments (dotenv-linter:on/off)
64    let mut disabled_checks: Vec<LintKind> = Vec::new();
65
66    let mut warnings: Vec<Warning> = Vec::new();
67
68    for line in lines {
69        if let Some(comment) = line.get_comment().and_then(Comment::parse) {
70            if comment.is_disabled() {
71                // Disable checks from a comment using the dotenv-linter:off flag
72                disabled_checks.extend(comment.checks);
73            } else {
74                // Enable checks if the comment has the dotenv-linter:on flag
75                disabled_checks.retain(|&s| !comment.checks.contains(&s));
76            }
77        }
78
79        for ch in &mut checks {
80            if line.is_comment() && ch.skip_comments() {
81                continue;
82            }
83
84            if disabled_checks.contains(&ch.name()) {
85                continue;
86            }
87
88            if let Some(warning) = ch.run(line) {
89                warnings.push(warning);
90            }
91        }
92    }
93
94    for ch in &mut checks {
95        let end_warns = ch.end();
96        warnings.extend(end_warns);
97    }
98
99    warnings
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::tests::{blank_line_entry, line_entry};
106
107    #[test]
108    fn run_with_empty_vec_test() {
109        let empty: Vec<LineEntry> = Vec::new();
110        let expected: Vec<Warning> = Vec::new();
111        let skip_checks: Vec<LintKind> = Vec::new();
112        assert_eq!(expected, check(&empty, &skip_checks, None));
113    }
114
115    #[test]
116    fn run_with_empty_line_test() {
117        let lines: Vec<LineEntry> = vec![blank_line_entry(1, 1)];
118        let expected: Vec<Warning> = Vec::new();
119        let skip_checks: Vec<LintKind> = Vec::new();
120        assert_eq!(expected, check(&lines, &skip_checks, None));
121    }
122
123    #[test]
124    fn run_with_comment_line_test() {
125        let lines: Vec<LineEntry> = vec![
126            line_entry(1, 2, "# Comment = 'Value'"),
127            blank_line_entry(2, 2),
128        ];
129        let expected: Vec<Warning> = Vec::new();
130        let skip_checks: Vec<LintKind> = Vec::new();
131        assert_eq!(expected, check(&lines, &skip_checks, None));
132    }
133
134    #[test]
135    fn run_with_valid_line_test() {
136        let lines: Vec<LineEntry> = vec![line_entry(1, 2, "FOO=BAR"), blank_line_entry(2, 2)];
137        let expected: Vec<Warning> = Vec::new();
138        let skip_checks: Vec<LintKind> = Vec::new();
139        assert_eq!(expected, check(&lines, &skip_checks, None));
140    }
141
142    #[test]
143    fn run_with_invalid_line_test() {
144        let line = line_entry(1, 2, "FOO");
145        let warning = Warning::new(
146            line.number,
147            LintKind::KeyWithoutValue,
148            "The FOO key should be with a value or have an equal sign",
149        );
150        let lines: Vec<LineEntry> = vec![line, blank_line_entry(2, 2)];
151        let expected: Vec<Warning> = vec![warning];
152        let skip_checks: Vec<LintKind> = Vec::new();
153        assert_eq!(expected, check(&lines, &skip_checks, None));
154    }
155
156    #[test]
157    fn run_without_blank_line_test() {
158        let line = line_entry(1, 1, "FOO=BAR");
159        let warning = Warning::new(
160            line.number,
161            LintKind::EndingBlankLine,
162            "No blank line at the end of the file",
163        );
164        let lines: Vec<LineEntry> = vec![line];
165        let expected: Vec<Warning> = vec![warning];
166        let skip_checks: Vec<LintKind> = Vec::new();
167        assert_eq!(expected, check(&lines, &skip_checks, None));
168    }
169
170    #[test]
171    fn skip_one_check() {
172        let line1 = line_entry(1, 3, "FOO\n");
173        let line2 = line_entry(2, 3, "1FOO\n");
174        let warning = Warning::new(
175            line2.number,
176            LintKind::LeadingCharacter,
177            "Invalid leading character detected",
178        );
179        let lines: Vec<LineEntry> = vec![line1, line2, blank_line_entry(3, 3)];
180        let expected: Vec<Warning> = vec![warning];
181        let skip_checks = vec![LintKind::KeyWithoutValue, LintKind::UnorderedKey];
182
183        assert_eq!(expected, check(&lines, &skip_checks, None));
184    }
185
186    #[test]
187    fn skip_all_checks() {
188        let line = line_entry(1, 1, "FOO");
189        let lines: Vec<LineEntry> = vec![line];
190        let expected: Vec<Warning> = Vec::new();
191        let skip_checks = vec![LintKind::KeyWithoutValue, LintKind::EndingBlankLine];
192
193        assert_eq!(expected, check(&lines, &skip_checks, None));
194    }
195
196    #[test]
197    fn skip_one_check_via_comment() {
198        let line1 = line_entry(1, 4, "# dotenv-linter:off KeyWithoutValue\n");
199        let line2 = line_entry(2, 4, "FOO\n");
200        let line3 = line_entry(3, 4, "1FOO\n");
201        let warning = Warning::new(
202            line3.number,
203            LintKind::LeadingCharacter,
204            "Invalid leading character detected",
205        );
206        let lines: Vec<LineEntry> = vec![line1, line2, line3, blank_line_entry(4, 4)];
207        let expected: Vec<Warning> = vec![warning];
208        let skip_checks = vec![LintKind::UnorderedKey];
209
210        assert_eq!(expected, check(&lines, &skip_checks, None));
211    }
212
213    #[test]
214    fn skip_collision() {
215        let line1 = line_entry(1, 4, "# dotenv-linter:on KeyWithoutValue\n");
216        let line2 = line_entry(2, 4, "FOO\n");
217        let line3 = line_entry(3, 4, "1FOO\n");
218        let warning = Warning::new(
219            line3.number,
220            LintKind::LeadingCharacter,
221            "Invalid leading character detected",
222        );
223        let lines: Vec<LineEntry> = vec![line1, line2, line3, blank_line_entry(4, 4)];
224        let expected: Vec<Warning> = vec![warning];
225        let skip_checks = vec![LintKind::KeyWithoutValue, LintKind::UnorderedKey];
226        assert_eq!(expected, check(&lines, &skip_checks, None));
227    }
228
229    #[test]
230    fn on_and_off_same_checks() {
231        let line1 = line_entry(
232            1,
233            5,
234            "# dotenv-linter:off KeyWithoutValue, LeadingCharacter\n",
235        );
236        let line2 = line_entry(2, 5, "FOO\n");
237        let line3 = line_entry(3, 5, "# dotenv-linter:on LeadingCharacter\n");
238        let line4 = line_entry(4, 5, "1FOO\n");
239        let warning = Warning::new(
240            line4.number,
241            LintKind::LeadingCharacter,
242            "Invalid leading character detected",
243        );
244        let lines: Vec<LineEntry> = vec![line1, line2, line3, line4, blank_line_entry(5, 5)];
245        let expected: Vec<Warning> = vec![warning];
246        let skip_checks: Vec<LintKind> = Vec::new();
247
248        assert_eq!(expected, check(&lines, &skip_checks, None));
249    }
250
251    #[test]
252    fn only_simple_comment() {
253        let line = line_entry(1, 1, "# Simple comment");
254        let warning = Warning::new(
255            line.number,
256            LintKind::EndingBlankLine,
257            "No blank line at the end of the file",
258        );
259        let lines: Vec<LineEntry> = vec![line];
260        let expected: Vec<Warning> = vec![warning];
261        let skip_checks: Vec<LintKind> = Vec::new();
262
263        assert_eq!(expected, check(&lines, &skip_checks, None));
264    }
265
266    #[test]
267    fn unordered_key_with_control_comment_test() {
268        let line_entries = vec![
269            line_entry(1, 7, "FOO=BAR"),
270            line_entry(2, 7, "# dotenv-linter:off LowercaseKey"),
271            line_entry(3, 7, "Bar=FOO"),
272            line_entry(4, 7, "bar=FOO"),
273            line_entry(5, 7, "# dotenv-linter:on LowercaseKey"),
274            line_entry(6, 7, "X=X"),
275            blank_line_entry(7, 7),
276        ];
277
278        let expected: Vec<Warning> = Vec::new();
279        let skip_checks: Vec<LintKind> = Vec::new();
280
281        assert_eq!(expected, check(&line_entries, &skip_checks, None));
282    }
283
284    mod schema {
285        use dotenv_core::LineEntry;
286        use dotenv_schema::DotEnvSchema;
287        use regex::Regex;
288
289        use crate::{LintKind, Warning, tests::line_entry};
290
291        fn load_schema() -> Result<DotEnvSchema, std::io::Error> {
292            let json = r#"{
293            "version": "1.0.0",
294            "entries": {
295                "NAME": {
296                    "type": "String"
297                },
298                "PORT": {
299                    "type": "Integer"
300                },
301                "PRICE": {
302                    "type": "Float"
303                },
304                "URL": {
305                    "type": "Url"
306                },
307                "EMAIL":{
308                    "type": "Email"
309                },
310                "FLAG":{
311                    "type": "Boolean"
312                }
313            }
314        }"#;
315            let schema: DotEnvSchema = serde_json::from_str(json)?;
316            Ok(schema)
317        }
318
319        #[test]
320        fn string_good() {
321            let schema = load_schema().expect("failed to load schema");
322            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "NAME=joe")];
323            let expected: Vec<Warning> = Vec::new();
324            let skip_checks: Vec<LintKind> = Vec::new();
325            assert_eq!(
326                expected,
327                crate::check::check(&lines, &skip_checks, Some(&schema))
328            );
329        }
330
331        #[test]
332        fn string_unknown() {
333            let schema = load_schema().expect("failed to load schema");
334            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "USER=joe")];
335            let expected: Vec<Warning> = vec![Warning::new(
336                1,
337                LintKind::SchemaViolation,
338                "The USER key is not defined in the schema",
339            )];
340            let skip_checks: Vec<LintKind> = Vec::new();
341            assert_eq!(
342                expected,
343                crate::check::check(&lines, &skip_checks, Some(&schema))
344            );
345        }
346
347        #[test]
348        fn string_unknown_allowed() {
349            let mut schema = load_schema().expect("failed to load schema");
350            schema.allow_other_keys = true;
351            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "USER=joe")];
352            let expected: Vec<Warning> = vec![];
353            let skip_checks: Vec<LintKind> = Vec::new();
354            assert_eq!(
355                expected,
356                crate::check::check(&lines, &skip_checks, Some(&schema))
357            );
358        }
359
360        #[test]
361        fn integer_good() {
362            let schema = load_schema().expect("failed to load schema");
363            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "PORT=42")];
364            let expected: Vec<Warning> = vec![];
365            let skip_checks: Vec<LintKind> = Vec::new();
366
367            assert_eq!(
368                expected,
369                crate::check::check(&lines, &skip_checks, Some(&schema))
370            );
371        }
372
373        #[test]
374        fn integer_bad() {
375            let schema = load_schema().expect("failed to load schema");
376            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "PORT=p")];
377            let expected: Vec<Warning> = vec![Warning::new(
378                1,
379                LintKind::SchemaViolation,
380                "The PORT key is not an integer",
381            )];
382            let skip_checks: Vec<LintKind> = Vec::new();
383            assert_eq!(
384                expected,
385                crate::check::check(&lines, &skip_checks, Some(&schema))
386            );
387        }
388
389        #[test]
390        fn integer_is_float() {
391            let schema = load_schema().expect("failed to load schema");
392            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "PORT=2.4")];
393            let expected: Vec<Warning> = vec![Warning::new(
394                1,
395                LintKind::SchemaViolation,
396                "The PORT key is not an integer",
397            )];
398            let skip_checks: Vec<LintKind> = Vec::new();
399            assert_eq!(
400                expected,
401                crate::check::check(&lines, &skip_checks, Some(&schema))
402            );
403        }
404
405        #[test]
406        fn float_good() {
407            let schema = load_schema().expect("failed to load schema");
408            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "PRICE=2.4")];
409            let expected: Vec<Warning> = vec![];
410            let skip_checks: Vec<LintKind> = Vec::new();
411            assert_eq!(
412                expected,
413                crate::check::check(&lines, &skip_checks, Some(&schema))
414            );
415        }
416
417        #[test]
418        fn float_good2() {
419            let schema = load_schema().expect("failed to load schema");
420            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "PRICE=24")];
421            let expected: Vec<Warning> = vec![];
422            let skip_checks: Vec<LintKind> = Vec::new();
423            assert_eq!(
424                expected,
425                crate::check::check(&lines, &skip_checks, Some(&schema))
426            );
427        }
428
429        #[test]
430        fn float_bad() {
431            let schema = load_schema().expect("failed to load schema");
432            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "PRICE=price")];
433            let expected: Vec<Warning> = vec![Warning::new(
434                1,
435                LintKind::SchemaViolation,
436                "The PRICE key is not a valid float",
437            )];
438            let skip_checks: Vec<LintKind> = Vec::new();
439            assert_eq!(
440                expected,
441                crate::check::check(&lines, &skip_checks, Some(&schema))
442            );
443        }
444
445        #[test]
446        fn url_good() {
447            let schema = load_schema().expect("failed to load schema");
448            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "URL=https://example.com")];
449            let expected: Vec<Warning> = vec![];
450            let skip_checks: Vec<LintKind> = Vec::new();
451            assert_eq!(
452                expected,
453                crate::check::check(&lines, &skip_checks, Some(&schema))
454            );
455        }
456
457        #[test]
458        fn url_bad() {
459            let schema = load_schema().expect("failed to load schema");
460            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "URL=not_a_url")];
461            let expected: Vec<Warning> = vec![Warning::new(
462                1,
463                LintKind::SchemaViolation,
464                "The URL key is not a valid URL",
465            )];
466            let skip_checks: Vec<LintKind> = Vec::new();
467            assert_eq!(
468                expected,
469                crate::check::check(&lines, &skip_checks, Some(&schema))
470            );
471        }
472
473        #[test]
474        fn email_good() {
475            let schema = load_schema().expect("failed to load schema");
476            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "EMAIL=joe@gmail.com")];
477            let expected: Vec<Warning> = vec![];
478            let skip_checks: Vec<LintKind> = Vec::new();
479            assert_eq!(
480                expected,
481                crate::check::check(&lines, &skip_checks, Some(&schema))
482            );
483        }
484
485        #[test]
486        fn email_bad() {
487            let schema = load_schema().expect("failed to load schema");
488            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "EMAIL=not_an_eamil")];
489            let expected: Vec<Warning> = vec![Warning::new(
490                1,
491                LintKind::SchemaViolation,
492                "The EMAIL key is not a valid email address",
493            )];
494            let skip_checks: Vec<LintKind> = Vec::new();
495            assert_eq!(
496                expected,
497                crate::check::check(&lines, &skip_checks, Some(&schema))
498            );
499        }
500
501        #[test]
502        fn required_present() {
503            let mut schema = load_schema().expect("failed to load schema");
504            schema.entries.get_mut("EMAIL").expect("get email").required = true;
505            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "EMAIL=joe@gmail.com")];
506            let expected: Vec<Warning> = vec![];
507            let skip_checks: Vec<LintKind> = Vec::new();
508            assert_eq!(
509                expected,
510                crate::check::check(&lines, &skip_checks, Some(&schema))
511            );
512        }
513
514        #[test]
515        fn required_missing() {
516            let mut schema = load_schema().expect("failed to load schema");
517            schema.entries.get_mut("EMAIL").expect("get email").required = true;
518            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "NAME=joe")];
519            let expected: Vec<Warning> = vec![Warning::new(
520                1,
521                LintKind::SchemaViolation,
522                "The EMAIL key is required",
523            )];
524            let skip_checks: Vec<LintKind> = Vec::new();
525            assert_eq!(
526                expected,
527                crate::check::check(&lines, &skip_checks, Some(&schema))
528            );
529        }
530
531        #[test]
532        fn regex_good() {
533            let mut schema = load_schema().expect("failed to load schema");
534            schema.entries.get_mut("NAME").expect("get email").regex =
535                Some(Regex::new("^[ABCD]*$").expect("Bad regex"));
536            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "NAME=BAD")];
537            let expected: Vec<Warning> = vec![];
538            let skip_checks: Vec<LintKind> = Vec::new();
539            assert_eq!(
540                expected,
541                crate::check::check(&lines, &skip_checks, Some(&schema))
542            );
543        }
544
545        #[test]
546        fn regex_bad() {
547            let mut schema = load_schema().expect("failed to load schema");
548            schema.entries.get_mut("NAME").expect("get email").regex =
549                Some(Regex::new("^[ABCD]*$").expect("Bad regex"));
550            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "NAME=joe")];
551            let expected: Vec<Warning> = vec![Warning::new(
552                1,
553                LintKind::SchemaViolation,
554                "The NAME key does not match the regex",
555            )];
556            let skip_checks: Vec<LintKind> = Vec::new();
557            assert_eq!(
558                expected,
559                crate::check::check(&lines, &skip_checks, Some(&schema))
560            );
561        }
562
563        #[test]
564        fn boolean_good() {
565            let schema = load_schema().expect("failed to load schema");
566            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "FLAG=true")];
567            let expected: Vec<Warning> = vec![];
568            let skip_checks: Vec<LintKind> = Vec::new();
569            assert_eq!(
570                expected,
571                crate::check::check(&lines, &skip_checks, Some(&schema))
572            );
573        }
574
575        #[test]
576        fn boolean_bad() {
577            let schema = load_schema().expect("failed to load schema");
578            let lines: Vec<LineEntry> = vec![line_entry(1, 2, "FLAG=joe")];
579            let expected: Vec<Warning> = vec![Warning::new(
580                1,
581                LintKind::SchemaViolation,
582                "The FLAG key is not a valid boolean",
583            )];
584            let skip_checks: Vec<LintKind> = Vec::new();
585            assert_eq!(
586                expected,
587                crate::check::check(&lines, &skip_checks, Some(&schema))
588            );
589        }
590    }
591}