mdlint/lint/rules/
md052.rs1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use pulldown_cmark::{Event, LinkType, Tag};
5use serde_json::Value;
6
7pub struct MD052;
8
9impl Rule for MD052 {
10 fn name(&self) -> &str {
11 "MD052"
12 }
13
14 fn description(&self) -> &str {
15 "Reference links and images should use a label that is defined"
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_broken_links() {
31 let (is_image, link_type, id) = match event {
32 Event::Start(Tag::Link { link_type, id, .. }) => (false, link_type, id),
33 Event::Start(Tag::Image { link_type, id, .. }) => (true, link_type, id),
34 _ => continue,
35 };
36
37 if matches!(
38 link_type,
39 LinkType::ReferenceUnknown | LinkType::CollapsedUnknown | LinkType::ShortcutUnknown
40 ) {
41 let item_type = if is_image { "image" } else { "link" };
42 violations.push(Violation {
43 line: parser.offset_to_line(range.start),
44 column: Some(1),
45 rule: self.name().to_string(),
46 message: format!("Reference {item_type} label '{id}' is not defined"),
47 fix: None,
48 });
49 }
50 }
51
52 violations
53 }
54
55 fn fixable(&self) -> bool {
56 false
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn test_defined_reference() {
66 let content = "[example]: https://example.com\n\n[Link][example]";
67 let parser = MarkdownParser::new(content);
68 let rule = MD052;
69 let violations = rule.check(&parser, None);
70
71 assert_eq!(violations.len(), 0);
72 }
73
74 #[test]
75 fn test_undefined_reference() {
76 let content = "[Link][undefined]";
77 let parser = MarkdownParser::new(content);
78 let rule = MD052;
79 let violations = rule.check(&parser, None);
80
81 assert_eq!(violations.len(), 1);
82 assert!(violations[0].message.contains("undefined"));
83 }
84
85 #[test]
86 fn test_image_reference() {
87 let content = "[img]: image.png\n\n![Alt][img]";
88 let parser = MarkdownParser::new(content);
89 let rule = MD052;
90 let violations = rule.check(&parser, None);
91
92 assert_eq!(violations.len(), 0);
93 }
94
95 #[test]
96 fn test_inline_links_ignored() {
97 let content = "[Link](https://example.com)";
98 let parser = MarkdownParser::new(content);
99 let rule = MD052;
100 let violations = rule.check(&parser, None);
101
102 assert_eq!(violations.len(), 0);
104 }
105
106 #[test]
107 fn test_case_insensitive() {
108 let content = "[EXAMPLE]: https://example.com\n\n[Link][example]";
109 let parser = MarkdownParser::new(content);
110 let rule = MD052;
111 let violations = rule.check(&parser, None);
112
113 assert_eq!(violations.len(), 0);
115 }
116
117 #[test]
118 fn test_shortcut_reference_undefined() {
119 let content = "Here a [reference] is unused.";
121 let parser = MarkdownParser::new(content);
122 let rule = MD052;
123 let violations = rule.check(&parser, None);
124
125 assert_eq!(violations.len(), 1);
126 assert!(violations[0].message.contains("reference"));
127 }
128
129 #[test]
130 fn test_shortcut_reference_defined() {
131 let content = "Here a [reference] is used.\n\n[reference]: https://example.com/";
132 let parser = MarkdownParser::new(content);
133 let rule = MD052;
134 let violations = rule.check(&parser, None);
135
136 assert_eq!(violations.len(), 0);
137 }
138
139 #[test]
140 fn test_collapsed_reference_undefined() {
141 let content = "[Link][]";
142 let parser = MarkdownParser::new(content);
143 let rule = MD052;
144 let violations = rule.check(&parser, None);
145
146 assert_eq!(violations.len(), 1);
147 }
148}