Skip to main content

mdlint/lint/rules/
md034.rs

1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use regex::Regex;
5use serde_json::Value;
6
7pub struct MD034;
8
9impl Rule for MD034 {
10    fn name(&self) -> &str {
11        "MD034"
12    }
13
14    fn description(&self) -> &str {
15        "Bare URL used"
16    }
17
18    fn tags(&self) -> &[&str] {
19        &["links", "url"]
20    }
21
22    fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
23        let mut violations = Vec::new();
24
25        // Regex to match URLs that aren't already in markdown link syntax
26        let url_regex = Regex::new(r"(?:^|[^(\[<`])((https?|ftp)://[^\s)\]>]+)").unwrap();
27
28        // Get code lines to skip (both blocks and inline code can contain URLs)
29        let code_lines = parser.get_code_line_numbers();
30        let ref_def_lines = parser.get_ref_def_line_numbers();
31
32        for (line_num, line) in parser.lines().iter().enumerate() {
33            let line_number = line_num + 1;
34
35            // Skip code and link reference definitions — URLs in these are not bare
36            if code_lines.contains(&line_number) || ref_def_lines.contains(&line_number) {
37                continue;
38            }
39
40            // Skip lines that are inside markdown link syntax
41            for cap in url_regex.captures_iter(line) {
42                if let Some(url_match) = cap.get(1) {
43                    let url = url_match.as_str();
44                    violations.push(Violation {
45                        line: line_number,
46                        column: Some(url_match.start() + 1),
47                        rule: self.name().to_string(),
48                        message: format!("Bare URL used: {}", url),
49                        fix: None,
50                    });
51                }
52            }
53        }
54
55        violations
56    }
57
58    fn fixable(&self) -> bool {
59        false
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn test_no_bare_url() {
69        let content = "Check out [my site](https://example.com)";
70        let parser = MarkdownParser::new(content);
71        let rule = MD034;
72        let violations = rule.check(&parser, None);
73
74        assert_eq!(violations.len(), 0);
75    }
76
77    #[test]
78    fn test_bare_url() {
79        let content = "Check out https://example.com for more info";
80        let parser = MarkdownParser::new(content);
81        let rule = MD034;
82        let violations = rule.check(&parser, None);
83
84        assert_eq!(violations.len(), 1);
85        assert!(violations[0].message.contains("https://example.com"));
86    }
87
88    #[test]
89    fn test_angle_bracket_url() {
90        let content = "Check out <https://example.com> for info";
91        let parser = MarkdownParser::new(content);
92        let rule = MD034;
93        let violations = rule.check(&parser, None);
94
95        assert_eq!(violations.len(), 0); // Angle brackets are OK
96    }
97
98    #[test]
99    fn test_multiple_urls() {
100        let content = "Visit https://example.com and https://test.com";
101        let parser = MarkdownParser::new(content);
102        let rule = MD034;
103        let violations = rule.check(&parser, None);
104
105        assert_eq!(violations.len(), 2);
106    }
107
108    #[test]
109    fn test_url_in_code_block() {
110        let content = "```shell\ncurl -LO https://example.com/file.tar.gz\n```";
111        let parser = MarkdownParser::new(content);
112        let rule = MD034;
113        let violations = rule.check(&parser, None);
114
115        assert_eq!(violations.len(), 0, "URLs in code blocks should be ignored");
116    }
117
118    #[test]
119    fn test_url_in_inline_code() {
120        let content = "Run `curl https://example.com` to download";
121        let parser = MarkdownParser::new(content);
122        let rule = MD034;
123        let violations = rule.check(&parser, None);
124
125        assert_eq!(violations.len(), 0, "URLs in inline code should be ignored");
126    }
127
128    #[test]
129    fn test_url_in_reference_definition() {
130        // Regression test for https://github.com/swanysimon/mdlint/issues/53
131        let content = "Here a [reference] is used.\n\n[reference]: https://example.com/";
132        let parser = MarkdownParser::new(content);
133        let rule = MD034;
134        let violations = rule.check(&parser, None);
135
136        assert_eq!(
137            violations.len(),
138            0,
139            "URL in a link reference definition should not be flagged as bare"
140        );
141    }
142}