Skip to main content

mdlint/lint/rules/
md042.rs

1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use pulldown_cmark::{Event, Tag};
5use serde_json::Value;
6
7pub struct MD042;
8
9impl Rule for MD042 {
10    fn name(&self) -> &'static str {
11        "MD042"
12    }
13
14    fn description(&self) -> &'static str {
15        "No empty links"
16    }
17
18    fn tags(&self) -> &[&str] {
19        &["links"]
20    }
21
22    fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
23        let mut violations = Vec::new();
24
25        for (event, range) in parser.parse_with_offsets() {
26            if let Event::Start(Tag::Link { dest_url, .. }) = event {
27                // Check if the destination URL is empty or only contains "#"
28                let url_str = dest_url.to_string();
29                if url_str.is_empty() || url_str == "#" {
30                    let line = parser.offset_to_line(range.start);
31                    violations.push(Violation {
32                        line,
33                        column: Some(1),
34                        rule: self.name().to_owned(),
35                        message: "No empty links".to_owned(),
36                        fix: None,
37                    });
38                }
39            }
40        }
41
42        violations
43    }
44
45    fn fixable(&self) -> bool {
46        false
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn test_link_with_text() {
56        let content = "[Link text](https://example.com)";
57        let parser = MarkdownParser::new(content);
58        let rule = MD042;
59        let violations = rule.check(&parser, None);
60
61        assert_eq!(violations.len(), 0);
62    }
63
64    #[test]
65    fn test_empty_destination() {
66        let content = "[Link text]()";
67        let parser = MarkdownParser::new(content);
68        let rule = MD042;
69        let violations = rule.check(&parser, None);
70
71        assert_eq!(violations.len(), 1);
72    }
73
74    #[test]
75    fn test_empty_text_non_empty_url() {
76        let content = "[](https://example.com)";
77        let parser = MarkdownParser::new(content);
78        let rule = MD042;
79        let violations = rule.check(&parser, None);
80
81        // Empty link text is fine, MD042 is about empty destinations
82        assert_eq!(violations.len(), 0);
83    }
84
85    #[test]
86    fn test_empty_fragment() {
87        let content = "[Link](#)";
88        let parser = MarkdownParser::new(content);
89        let rule = MD042;
90        let violations = rule.check(&parser, None);
91
92        // Empty fragment should trigger MD042
93        assert_eq!(violations.len(), 1);
94    }
95
96    #[test]
97    fn test_multiple_links() {
98        let content = "[Good](url1) and [Bad]() and [Also good](url3)";
99        let parser = MarkdownParser::new(content);
100        let rule = MD042;
101        let violations = rule.check(&parser, None);
102
103        assert_eq!(violations.len(), 1); // Only second link has empty destination
104    }
105}