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
21pub 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
33fn 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 checks.retain(|c| !skip_checks.contains(&c.name()));
62
63 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 disabled_checks.extend(comment.checks);
73 } else {
74 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}