use rumdl_lib::config::MarkdownFlavor;
use rumdl_lib::rule::{LintWarning, Rule};
use rumdl_lib::rules::md013_line_length::md013_config::{MD013Config, ReflowMode};
use rumdl_lib::rules::{MD009TrailingSpaces, MD012NoMultipleBlanks, MD013LineLength, MD087UnusedDisableComment};
fn lint(content: &str, rules: &[Box<dyn Rule>]) -> Vec<LintWarning> {
rumdl_lib::lint(content, rules, false, MarkdownFlavor::Standard, None, None).unwrap()
}
fn md013_and_md087() -> Vec<Box<dyn Rule>> {
vec![
Box::new(MD013LineLength::default()),
Box::new(MD087UnusedDisableComment::new()),
]
}
fn messages(warnings: &[LintWarning]) -> Vec<&str> {
warnings.iter().map(|warning| warning.message.as_str()).collect()
}
fn over_long() -> String {
"word ".repeat(30)
}
#[test]
fn unused_disable_line_is_reported() {
let content = "Short line. <!-- rumdl-disable-line MD013 -->\n";
let warnings = lint(content, &md013_and_md087());
assert_eq!(messages(&warnings), ["Unused disable-line comment: MD013"]);
assert_eq!(warnings[0].line, 1);
assert_eq!(warnings[0].column, 13);
assert_eq!(warnings[0].end_column, 46);
assert_eq!(warnings[0].rule_name.as_deref(), Some("MD087"));
}
#[test]
fn a_comment_that_suppresses_a_finding_is_left_alone() {
let used = format!("{} <!-- rumdl-disable-line MD013 -->\n", over_long());
assert!(
lint(&used, &md013_and_md087()).is_empty(),
"the comment silences MD013 on this line, so it is doing its job"
);
let without_comment = format!("{}\n", over_long());
assert_eq!(
lint(&without_comment, &md013_and_md087()).len(),
1,
"without the comment the line reports"
);
}
#[test]
fn a_rule_the_run_does_not_carry_is_not_judged() {
let content = "Short line. <!-- rumdl-disable-line MD013 -->\n";
let md087_only: Vec<Box<dyn Rule>> = vec![Box::new(MD087UnusedDisableComment::new())];
assert!(
lint(content, &md087_only).is_empty(),
"MD013 is absent from the run, so its silence proves nothing"
);
assert_eq!(
lint(content, &md013_and_md087()).len(),
1,
"control: the same comment is judged once MD013 runs"
);
}
#[test]
fn disable_next_line_is_judged_against_the_following_line() {
let unused = "<!-- rumdl-disable-next-line MD013 -->\nshort\n";
assert_eq!(
messages(&lint(unused, &md013_and_md087())),
["Unused disable-next-line comment: MD013"]
);
let used = format!("<!-- rumdl-disable-next-line MD013 -->\n{}\n", over_long());
assert!(lint(&used, &md013_and_md087()).is_empty());
}
#[test]
fn a_block_disable_is_judged_to_the_end_of_the_document() {
let unused = "<!-- rumdl-disable MD013 -->\n\nshort\n\nalso short\n";
assert_eq!(
messages(&lint(unused, &md013_and_md087())),
["Unused disable comment: MD013"]
);
let used = format!("<!-- rumdl-disable MD013 -->\n\nshort\n\n{}\n", over_long());
assert!(
lint(&used, &md013_and_md087()).is_empty(),
"a finding anywhere below the comment keeps it alive"
);
}
#[test]
fn disable_file_covers_lines_above_the_comment() {
let used = format!("{}\n\n<!-- rumdl-disable-file MD013 -->\n", over_long());
assert!(
lint(&used, &md013_and_md087()).is_empty(),
"a file-scope comment reaches back over the whole document"
);
let unused = "short\n\n<!-- rumdl-disable-file MD013 -->\n";
assert_eq!(
messages(&lint(unused, &md013_and_md087())),
["Unused disable-file comment: MD013"]
);
}
#[test]
fn a_configure_file_entry_set_to_false_is_judged_like_a_disable() {
let unused = "<!-- rumdl-configure-file { \"MD013\": false } -->\n\nshort\n";
assert_eq!(
messages(&lint(unused, &md013_and_md087())),
["Unused configure-file disable: MD013"]
);
let used = format!(
"<!-- rumdl-configure-file {{ \"MD013\": false }} -->\n\n{}\n",
over_long()
);
assert!(lint(&used, &md013_and_md087()).is_empty());
}
#[test]
fn a_comment_a_wider_one_already_covers_is_reported() {
for wider in ["<!-- rumdl-disable-file MD013 -->", "<!-- rumdl-disable MD013 -->"] {
let content = format!("{wider}\n\n{} <!-- rumdl-disable-line MD013 -->\n", over_long());
assert_eq!(
messages(&lint(&content, &md013_and_md087())),
["Unused disable-line comment: MD013"],
"with {wider} above it"
);
}
}
#[test]
fn two_comments_covering_different_findings_are_both_left_alone() {
let content = format!(
"{} <!-- rumdl-disable-line MD013 -->\n<!-- rumdl-disable MD013 -->\n{}\n",
over_long(),
over_long()
);
assert!(lint(&content, &md013_and_md087()).is_empty());
}
#[test]
fn a_block_disable_a_later_enable_closes_is_reported() {
let content = format!(
"<!-- rumdl-disable MD013 -->\n<!-- rumdl-enable MD013 -->\n{} <!-- rumdl-disable-line MD013 -->\n",
over_long()
);
assert_eq!(
messages(&lint(&content, &md013_and_md087())),
["Unused disable comment: MD013"]
);
}
fn md013_reflow_and_md087() -> Vec<Box<dyn Rule>> {
vec![
Box::new(MD013LineLength::from_config_struct(MD013Config {
reflow: true,
reflow_mode: ReflowMode::Normalize,
..Default::default()
})),
Box::new(MD087UnusedDisableComment::new()),
]
}
#[test]
fn only_the_first_covered_line_of_a_multi_line_finding_credits_a_comment() {
let first = "This paragraph line is quite long and would normally be reflowed by the rule into shorter lines.";
let second = "Second line of that same paragraph, also fairly long so that the whole block moves around a lot.";
let comment = "<!-- rumdl-disable-line MD013 -->";
let rules = md013_reflow_and_md087();
let warnings = lint(&format!("{first}\n{second}\n"), &rules);
assert_eq!(warnings.len(), 1, "got: {warnings:?}");
assert_eq!(
(warnings[0].line, warnings[0].end_line),
(1, 2),
"the finding has to span both lines for this test to mean anything"
);
for content in [
format!("{first} {comment}\n{second}\n"),
format!("{first}\n{second} {comment}\n"),
] {
assert!(
lint(&content, &rules).is_empty(),
"one comment does the whole job: {content:?}"
);
}
let warnings = lint(&format!("{first} {comment}\n{second} {comment}\n"), &rules);
assert_eq!(messages(&warnings), ["Unused disable-line comment: MD013"]);
assert_eq!(warnings[0].line, 2);
}
#[test]
fn a_configure_file_entry_carrying_options_is_left_alone() {
let content = "<!-- rumdl-configure-file { \"MD013\": { \"line_length\": 200 } } -->\n\nshort\n";
assert!(lint(content, &md013_and_md087()).is_empty());
}
#[test]
fn only_the_names_that_suppressed_nothing_are_reported() {
let rules: Vec<Box<dyn Rule>> = vec![
Box::new(MD009TrailingSpaces::default()),
Box::new(MD013LineLength::default()),
Box::new(MD087UnusedDisableComment::new()),
];
let content = "<!-- rumdl-disable-next-line MD009 MD013 -->\nshort line \n";
assert_eq!(
messages(&lint(content, &rules)),
["Unused disable-next-line comment: MD013"],
"MD009 fires on the covered line, MD013 does not"
);
let without_comment = "short line \n";
assert_eq!(lint(without_comment, &rules).len(), 1);
}
#[test]
fn a_comment_naming_no_rule_is_never_reported() {
for content in [
"Short line. <!-- rumdl-disable-line -->\n",
"<!-- rumdl-disable -->\n\nshort\n",
"<!-- rumdl-disable-file -->\n\nshort\n",
] {
assert!(
lint(content, &md013_and_md087()).is_empty(),
"a rule-less comment covers rules this run may not carry: {content:?}"
);
}
}
#[test]
fn a_comment_inside_a_code_block_is_not_a_comment() {
let content = "```markdown\nShort line. <!-- rumdl-disable-line MD013 -->\n```\n";
assert!(lint(content, &md013_and_md087()).is_empty());
}
#[test]
fn an_unknown_rule_name_is_left_to_the_inline_config_warning() {
let content = "Short line. <!-- rumdl-disable-line MD999 -->\n";
assert!(lint(content, &md013_and_md087()).is_empty());
}
#[test]
fn a_prettier_ignore_comment_belongs_to_another_formatter() {
let content = "<!-- prettier-ignore -->\nshort\n";
assert!(lint(content, &md013_and_md087()).is_empty());
}
#[test]
fn the_markdownlint_prefix_is_judged_the_same_way() {
let content = "Short line. <!-- markdownlint-disable-line MD013 -->\n";
assert_eq!(
messages(&lint(content, &md013_and_md087())),
["Unused disable-line comment: MD013"]
);
}
#[test]
fn an_alias_is_reported_as_the_author_wrote_it() {
let content = "Short line. <!-- rumdl-disable-line line-length -->\n";
assert_eq!(
messages(&lint(content, &md013_and_md087())),
["Unused disable-line comment: line-length"]
);
let used = format!("{} <!-- rumdl-disable-line line-length -->\n", over_long());
assert!(
lint(&used, &md013_and_md087()).is_empty(),
"the alias disables the same rule it is judged against"
);
}
#[test]
fn md087_can_be_disabled_inline_like_any_other_rule() {
let content = "Short. <!-- rumdl-disable-line MD013 --> <!-- rumdl-disable-line MD087 -->\n";
assert!(lint(content, &md013_and_md087()).is_empty());
let without = "Short. <!-- rumdl-disable-line MD013 -->\n";
assert_eq!(
lint(without, &md013_and_md087()).len(),
1,
"control: the finding is there to be suppressed"
);
}
#[test]
fn a_comment_is_judged_per_rule_it_names() {
let rules: Vec<Box<dyn Rule>> = vec![
Box::new(MD012NoMultipleBlanks::default()),
Box::new(MD013LineLength::default()),
Box::new(MD087UnusedDisableComment::new()),
];
let content = format!(
"{} <!-- rumdl-disable-line MD013 -->\n\n\n<!-- rumdl-disable-next-line MD012 -->\nlast\n",
over_long()
);
let warnings = lint(&content, &rules);
let verdicts: Vec<&str> = warnings
.iter()
.filter(|warning| warning.rule_name.as_deref() == Some("MD087"))
.map(|warning| warning.message.as_str())
.collect();
assert_eq!(
verdicts,
["Unused disable-next-line comment: MD012"],
"MD013's comment silences the long line; MD012 reports above its comment, not below it"
);
assert!(
warnings
.iter()
.any(|warning| warning.rule_name.as_deref() == Some("MD012")),
"control: MD012 is reporting, which is what makes its comment's silence meaningful"
);
}
#[test]
fn check_on_its_own_reports_nothing() {
let rule = MD087UnusedDisableComment::new();
let ctx = rumdl_lib::lint_context::LintContext::new(
"Short line. <!-- rumdl-disable-line MD013 -->\n",
MarkdownFlavor::Standard,
None,
);
assert!(rule.check(&ctx).unwrap().is_empty());
}