use dotenv_core::LineEntry;
use dotenv_schema::DotEnvSchema;
use crate::{Comment, LintKind, Warning};
mod duplicated_key;
mod ending_blank_line;
mod extra_blank_line;
mod incorrect_delimiter;
mod key_without_value;
mod leading_character;
mod lowercase_key;
mod quote_character;
mod schema_violation;
mod space_character;
mod substitution_key;
mod trailing_whitespace;
mod unordered_key;
mod value_without_quotes;
pub trait Check {
fn run(&mut self, line: &LineEntry) -> Option<Warning>;
fn name(&self) -> LintKind;
fn skip_comments(&self) -> bool {
true
}
fn end(&mut self) -> Vec<Warning> {
vec![]
}
}
fn checklist<'a>(schema: Option<&'a DotEnvSchema>) -> Vec<Box<dyn Check + 'a>> {
vec![
Box::<duplicated_key::DuplicatedKeyChecker>::default(),
Box::<ending_blank_line::EndingBlankLineChecker>::default(),
Box::<extra_blank_line::ExtraBlankLineChecker>::default(),
Box::<incorrect_delimiter::IncorrectDelimiterChecker>::default(),
Box::<key_without_value::KeyWithoutValueChecker>::default(),
Box::<leading_character::LeadingCharacterChecker>::default(),
Box::<lowercase_key::LowercaseKeyChecker>::default(),
Box::<quote_character::QuoteCharacterChecker>::default(),
Box::<space_character::SpaceCharacterChecker>::default(),
Box::<substitution_key::SubstitutionKeyChecker>::default(),
Box::<trailing_whitespace::TrailingWhitespaceChecker>::default(),
Box::<unordered_key::UnorderedKeyChecker>::default(),
Box::<value_without_quotes::ValueWithoutQuotesChecker>::default(),
Box::new(schema_violation::SchemaViolationChecker::new(schema)),
]
}
pub fn check(
lines: &[LineEntry],
skip_checks: &[LintKind],
schema: Option<&DotEnvSchema>,
) -> Vec<Warning> {
let mut checks = checklist(schema);
checks.retain(|c| !skip_checks.contains(&c.name()));
let mut disabled_checks: Vec<LintKind> = Vec::new();
let mut warnings: Vec<Warning> = Vec::new();
for line in lines {
if let Some(comment) = line.get_comment().and_then(Comment::parse) {
if comment.is_disabled() {
disabled_checks.extend(comment.checks);
} else {
disabled_checks.retain(|&s| !comment.checks.contains(&s));
}
}
for ch in &mut checks {
if line.is_comment() && ch.skip_comments() {
continue;
}
if disabled_checks.contains(&ch.name()) {
continue;
}
if let Some(warning) = ch.run(line) {
warnings.push(warning);
}
}
}
for ch in &mut checks {
let end_warns = ch.end();
warnings.extend(end_warns);
}
warnings
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{blank_line_entry, line_entry};
#[test]
fn run_with_empty_vec_test() {
let empty: Vec<LineEntry> = Vec::new();
let expected: Vec<Warning> = Vec::new();
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(expected, check(&empty, &skip_checks, None));
}
#[test]
fn run_with_empty_line_test() {
let lines: Vec<LineEntry> = vec![blank_line_entry(1, 1)];
let expected: Vec<Warning> = Vec::new();
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(expected, check(&lines, &skip_checks, None));
}
#[test]
fn run_with_comment_line_test() {
let lines: Vec<LineEntry> = vec![
line_entry(1, 2, "# Comment = 'Value'"),
blank_line_entry(2, 2),
];
let expected: Vec<Warning> = Vec::new();
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(expected, check(&lines, &skip_checks, None));
}
#[test]
fn run_with_valid_line_test() {
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "FOO=BAR"), blank_line_entry(2, 2)];
let expected: Vec<Warning> = Vec::new();
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(expected, check(&lines, &skip_checks, None));
}
#[test]
fn run_with_invalid_line_test() {
let line = line_entry(1, 2, "FOO");
let warning = Warning::new(
line.number,
LintKind::KeyWithoutValue,
"The FOO key should be with a value or have an equal sign",
);
let lines: Vec<LineEntry> = vec![line, blank_line_entry(2, 2)];
let expected: Vec<Warning> = vec![warning];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(expected, check(&lines, &skip_checks, None));
}
#[test]
fn run_without_blank_line_test() {
let line = line_entry(1, 1, "FOO=BAR");
let warning = Warning::new(
line.number,
LintKind::EndingBlankLine,
"No blank line at the end of the file",
);
let lines: Vec<LineEntry> = vec![line];
let expected: Vec<Warning> = vec![warning];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(expected, check(&lines, &skip_checks, None));
}
#[test]
fn skip_one_check() {
let line1 = line_entry(1, 3, "FOO\n");
let line2 = line_entry(2, 3, "1FOO\n");
let warning = Warning::new(
line2.number,
LintKind::LeadingCharacter,
"Invalid leading character detected",
);
let lines: Vec<LineEntry> = vec![line1, line2, blank_line_entry(3, 3)];
let expected: Vec<Warning> = vec![warning];
let skip_checks = vec![LintKind::KeyWithoutValue, LintKind::UnorderedKey];
assert_eq!(expected, check(&lines, &skip_checks, None));
}
#[test]
fn skip_all_checks() {
let line = line_entry(1, 1, "FOO");
let lines: Vec<LineEntry> = vec![line];
let expected: Vec<Warning> = Vec::new();
let skip_checks = vec![LintKind::KeyWithoutValue, LintKind::EndingBlankLine];
assert_eq!(expected, check(&lines, &skip_checks, None));
}
#[test]
fn skip_one_check_via_comment() {
let line1 = line_entry(1, 4, "# dotenv-linter:off KeyWithoutValue\n");
let line2 = line_entry(2, 4, "FOO\n");
let line3 = line_entry(3, 4, "1FOO\n");
let warning = Warning::new(
line3.number,
LintKind::LeadingCharacter,
"Invalid leading character detected",
);
let lines: Vec<LineEntry> = vec![line1, line2, line3, blank_line_entry(4, 4)];
let expected: Vec<Warning> = vec![warning];
let skip_checks = vec![LintKind::UnorderedKey];
assert_eq!(expected, check(&lines, &skip_checks, None));
}
#[test]
fn skip_collision() {
let line1 = line_entry(1, 4, "# dotenv-linter:on KeyWithoutValue\n");
let line2 = line_entry(2, 4, "FOO\n");
let line3 = line_entry(3, 4, "1FOO\n");
let warning = Warning::new(
line3.number,
LintKind::LeadingCharacter,
"Invalid leading character detected",
);
let lines: Vec<LineEntry> = vec![line1, line2, line3, blank_line_entry(4, 4)];
let expected: Vec<Warning> = vec![warning];
let skip_checks = vec![LintKind::KeyWithoutValue, LintKind::UnorderedKey];
assert_eq!(expected, check(&lines, &skip_checks, None));
}
#[test]
fn on_and_off_same_checks() {
let line1 = line_entry(
1,
5,
"# dotenv-linter:off KeyWithoutValue, LeadingCharacter\n",
);
let line2 = line_entry(2, 5, "FOO\n");
let line3 = line_entry(3, 5, "# dotenv-linter:on LeadingCharacter\n");
let line4 = line_entry(4, 5, "1FOO\n");
let warning = Warning::new(
line4.number,
LintKind::LeadingCharacter,
"Invalid leading character detected",
);
let lines: Vec<LineEntry> = vec![line1, line2, line3, line4, blank_line_entry(5, 5)];
let expected: Vec<Warning> = vec![warning];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(expected, check(&lines, &skip_checks, None));
}
#[test]
fn only_simple_comment() {
let line = line_entry(1, 1, "# Simple comment");
let warning = Warning::new(
line.number,
LintKind::EndingBlankLine,
"No blank line at the end of the file",
);
let lines: Vec<LineEntry> = vec![line];
let expected: Vec<Warning> = vec![warning];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(expected, check(&lines, &skip_checks, None));
}
#[test]
fn unordered_key_with_control_comment_test() {
let line_entries = vec![
line_entry(1, 7, "FOO=BAR"),
line_entry(2, 7, "# dotenv-linter:off LowercaseKey"),
line_entry(3, 7, "Bar=FOO"),
line_entry(4, 7, "bar=FOO"),
line_entry(5, 7, "# dotenv-linter:on LowercaseKey"),
line_entry(6, 7, "X=X"),
blank_line_entry(7, 7),
];
let expected: Vec<Warning> = Vec::new();
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(expected, check(&line_entries, &skip_checks, None));
}
mod schema {
use dotenv_core::LineEntry;
use dotenv_schema::DotEnvSchema;
use regex::Regex;
use crate::{LintKind, Warning, tests::line_entry};
fn load_schema() -> Result<DotEnvSchema, std::io::Error> {
let json = r#"{
"version": "1.0.0",
"entries": {
"NAME": {
"type": "String"
},
"PORT": {
"type": "Integer"
},
"PRICE": {
"type": "Float"
},
"URL": {
"type": "Url"
},
"EMAIL":{
"type": "Email"
},
"FLAG":{
"type": "Boolean"
}
}
}"#;
let schema: DotEnvSchema = serde_json::from_str(json)?;
Ok(schema)
}
#[test]
fn string_good() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "NAME=joe")];
let expected: Vec<Warning> = Vec::new();
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn string_unknown() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "USER=joe")];
let expected: Vec<Warning> = vec![Warning::new(
1,
LintKind::SchemaViolation,
"The USER key is not defined in the schema",
)];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn string_unknown_allowed() {
let mut schema = load_schema().expect("failed to load schema");
schema.allow_other_keys = true;
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "USER=joe")];
let expected: Vec<Warning> = vec![];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn integer_good() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "PORT=42")];
let expected: Vec<Warning> = vec![];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn integer_bad() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "PORT=p")];
let expected: Vec<Warning> = vec![Warning::new(
1,
LintKind::SchemaViolation,
"The PORT key is not an integer",
)];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn integer_is_float() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "PORT=2.4")];
let expected: Vec<Warning> = vec![Warning::new(
1,
LintKind::SchemaViolation,
"The PORT key is not an integer",
)];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn float_good() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "PRICE=2.4")];
let expected: Vec<Warning> = vec![];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn float_good2() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "PRICE=24")];
let expected: Vec<Warning> = vec![];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn float_bad() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "PRICE=price")];
let expected: Vec<Warning> = vec![Warning::new(
1,
LintKind::SchemaViolation,
"The PRICE key is not a valid float",
)];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn url_good() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "URL=https://example.com")];
let expected: Vec<Warning> = vec![];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn url_bad() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "URL=not_a_url")];
let expected: Vec<Warning> = vec![Warning::new(
1,
LintKind::SchemaViolation,
"The URL key is not a valid URL",
)];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn email_good() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "EMAIL=joe@gmail.com")];
let expected: Vec<Warning> = vec![];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn email_bad() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "EMAIL=not_an_eamil")];
let expected: Vec<Warning> = vec![Warning::new(
1,
LintKind::SchemaViolation,
"The EMAIL key is not a valid email address",
)];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn required_present() {
let mut schema = load_schema().expect("failed to load schema");
schema.entries.get_mut("EMAIL").expect("get email").required = true;
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "EMAIL=joe@gmail.com")];
let expected: Vec<Warning> = vec![];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn required_missing() {
let mut schema = load_schema().expect("failed to load schema");
schema.entries.get_mut("EMAIL").expect("get email").required = true;
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "NAME=joe")];
let expected: Vec<Warning> = vec![Warning::new(
1,
LintKind::SchemaViolation,
"The EMAIL key is required",
)];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn regex_good() {
let mut schema = load_schema().expect("failed to load schema");
schema.entries.get_mut("NAME").expect("get email").regex =
Some(Regex::new("^[ABCD]*$").expect("Bad regex"));
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "NAME=BAD")];
let expected: Vec<Warning> = vec![];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn regex_bad() {
let mut schema = load_schema().expect("failed to load schema");
schema.entries.get_mut("NAME").expect("get email").regex =
Some(Regex::new("^[ABCD]*$").expect("Bad regex"));
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "NAME=joe")];
let expected: Vec<Warning> = vec![Warning::new(
1,
LintKind::SchemaViolation,
"The NAME key does not match the regex",
)];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn boolean_good() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "FLAG=true")];
let expected: Vec<Warning> = vec![];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
#[test]
fn boolean_bad() {
let schema = load_schema().expect("failed to load schema");
let lines: Vec<LineEntry> = vec![line_entry(1, 2, "FLAG=joe")];
let expected: Vec<Warning> = vec![Warning::new(
1,
LintKind::SchemaViolation,
"The FLAG key is not a valid boolean",
)];
let skip_checks: Vec<LintKind> = Vec::new();
assert_eq!(
expected,
crate::check::check(&lines, &skip_checks, Some(&schema))
);
}
}
}