mdlint/lint/rules/
md034.rs1use 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 let url_regex = Regex::new(r"(?:^|[^(\[<`])((https?|ftp)://[^\s)\]>]+)").unwrap();
27
28 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 if code_lines.contains(&line_number) || ref_def_lines.contains(&line_number) {
37 continue;
38 }
39
40 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); }
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 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}