1use crate::rule::{LintError, LintResult, LintWarning, Rule, Severity};
2use crate::utils::mkdocs_patterns::is_mkdocs_auto_reference;
3use crate::utils::range_utils::calculate_match_range;
4use crate::utils::regex_cache::{HTML_COMMENT_PATTERN, SHORTCUT_REF_REGEX};
5use crate::utils::skip_context::{is_in_math_context, is_in_table_cell};
6use regex::Regex;
7use std::collections::{HashMap, HashSet};
8use std::sync::LazyLock;
9
10mod md052_config;
11use md052_config::MD052Config;
12
13static REF_REGEX: LazyLock<Regex> =
17 LazyLock::new(|| Regex::new(r"^\s*\[((?:[^\[\]\\]|\\.|\[[^\]]*\])*)\]:\s*.*").unwrap());
18
19static LIST_ITEM_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*[-*+]\s+(?:\[[xX\s]\]\s+)?").unwrap());
21
22static FENCED_CODE_START: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)(`{3,}|~{3,})").unwrap());
24
25static OUTPUT_EXAMPLE_START: LazyLock<Regex> =
27 LazyLock::new(|| Regex::new(r"^#+\s*(?:Output|Example|Output Style|Output Format)\s*$").unwrap());
28
29static GITHUB_ALERT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
32 Regex::new(r"^\s*>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION|INFO|SUCCESS|FAILURE|DANGER|BUG|EXAMPLE|QUOTE)\]")
33 .unwrap()
34});
35
36static URL_WITH_BRACKETS: LazyLock<Regex> =
44 LazyLock::new(|| Regex::new(r"https?://(?:\[[0-9a-fA-F:.%]+\]|[^\s\[\]]+/[^\s]*\[\d+\])").unwrap());
45
46#[derive(Clone, Default)]
59pub struct MD052ReferenceLinkImages {
60 config: MD052Config,
61}
62
63impl MD052ReferenceLinkImages {
64 pub fn new() -> Self {
65 Self {
66 config: MD052Config::default(),
67 }
68 }
69
70 pub fn from_config_struct(config: MD052Config) -> Self {
71 Self { config }
72 }
73
74 fn strip_backticks(s: &str) -> &str {
77 s.trim_start_matches('`').trim_end_matches('`')
78 }
79
80 fn is_valid_python_identifier(s: &str) -> bool {
84 if s.is_empty() {
85 return false;
86 }
87 let first_char = s.chars().next().unwrap();
88 if !first_char.is_ascii_alphabetic() && first_char != '_' {
89 return false;
90 }
91 s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
92 }
93
94 fn is_known_non_reference_pattern(&self, text: &str) -> bool {
103 if self.config.ignore.iter().any(|p| p.eq_ignore_ascii_case(text)) {
107 return true;
108 }
109 if text.chars().all(|c| c.is_ascii_digit()) {
111 return true;
112 }
113
114 if text.contains(':') && text.chars().all(|c| c.is_ascii_digit() || c == ':') {
116 return true;
117 }
118
119 if text.contains('.')
123 && !text.contains(' ')
124 && !text.contains('-')
125 && !text.contains('_')
126 && !text.contains('`')
127 {
128 return true;
130 }
131
132 if text == "*" || text == "..." || text == "**" {
134 return true;
135 }
136
137 if text.contains('/') && !text.contains(' ') && !text.starts_with("http") {
139 return true;
140 }
141
142 if text.contains(',') || text.contains('[') || text.contains(']') {
145 return true;
147 }
148
149 if !text.contains('`')
156 && text.contains('.')
157 && !text.contains(' ')
158 && !text.contains('-')
159 && !text.contains('_')
160 {
161 return true;
162 }
163
164 if text.chars().all(|c| !c.is_alphanumeric() && c != ' ') {
171 return true;
172 }
173
174 if text.len() <= 2 && !text.chars().all(|c| c.is_alphabetic()) {
176 return true;
177 }
178
179 if (text.starts_with('"') && text.ends_with('"'))
181 || (text.starts_with('\'') && text.ends_with('\''))
182 || text.contains('"')
183 || text.contains('\'')
184 {
185 return true;
186 }
187
188 if text.contains(':') && text.contains(' ') {
191 return true;
192 }
193
194 if text.starts_with('!') {
196 return true;
197 }
198
199 if text.starts_with('^') {
202 return true;
203 }
204
205 if text.starts_with('@') {
208 return true;
209 }
210
211 if text == "TOC" {
214 return true;
215 }
216
217 if text.len() == 1 && text.chars().all(|c| c.is_ascii_uppercase()) {
219 return true;
220 }
221
222 let common_non_refs = [
225 "object",
227 "Object",
228 "any",
229 "Any",
230 "inv",
231 "void",
232 "bool",
233 "int",
234 "float",
235 "str",
236 "char",
237 "i8",
238 "i16",
239 "i32",
240 "i64",
241 "i128",
242 "isize",
243 "u8",
244 "u16",
245 "u32",
246 "u64",
247 "u128",
248 "usize",
249 "f32",
250 "f64",
251 "null",
253 "true",
254 "false",
255 "NaN",
256 "Infinity",
257 "object Object",
259 ];
260
261 if common_non_refs.contains(&text) {
262 return true;
263 }
264
265 false
266 }
267
268 fn is_in_code_span(line: usize, col: usize, code_spans: &[crate::lint_context::CodeSpan]) -> bool {
270 code_spans
271 .iter()
272 .any(|span| span.line == line && col >= span.start_col && col < span.end_col)
273 }
274
275 fn is_in_html_comment(content: &str, byte_pos: usize) -> bool {
277 for m in HTML_COMMENT_PATTERN.find_iter(content) {
278 if m.start() <= byte_pos && byte_pos < m.end() {
279 return true;
280 }
281 }
282 false
283 }
284
285 fn is_in_html_tag(ctx: &crate::lint_context::LintContext, byte_pos: usize) -> bool {
287 for html_tag in ctx.html_tags().iter() {
289 if html_tag.byte_offset <= byte_pos && byte_pos < html_tag.byte_end {
290 return true;
291 }
292 }
293 false
294 }
295
296 fn extract_references(&self, content: &str, mkdocs_mode: bool) -> HashSet<String> {
297 use crate::config::MarkdownFlavor;
298 use crate::utils::skip_context::is_mkdocs_snippet_line;
299
300 let mut references = HashSet::new();
301 let mut in_code_block = false;
302 let mut code_fence_marker = String::new();
303
304 for line in content.lines() {
305 if is_mkdocs_snippet_line(
307 line,
308 if mkdocs_mode {
309 MarkdownFlavor::MkDocs
310 } else {
311 MarkdownFlavor::Standard
312 },
313 ) {
314 continue;
315 }
316 if let Some(cap) = FENCED_CODE_START.captures(line) {
318 if let Some(fence) = cap.get(2) {
319 let fence_str = fence.as_str();
321 if !in_code_block {
322 in_code_block = true;
323 code_fence_marker = fence_str.to_string();
324 } else if line.trim_start().starts_with(&code_fence_marker) {
325 let trimmed = line.trim_start();
327 if trimmed.starts_with(&code_fence_marker) {
329 let after_fence = &trimmed[code_fence_marker.len()..];
330 if after_fence.trim().is_empty() {
331 in_code_block = false;
332 code_fence_marker.clear();
333 }
334 }
335 }
336 }
337 continue;
338 }
339
340 if in_code_block {
342 continue;
343 }
344
345 if line.trim_start().starts_with("*[") {
348 continue;
349 }
350
351 if let Some(cap) = REF_REGEX.captures(line) {
352 if let Some(reference) = cap.get(1) {
354 references.insert(reference.as_str().to_lowercase());
355 }
356 }
357 }
358
359 references
360 }
361
362 fn find_undefined_references(
363 &self,
364 content: &str,
365 references: &HashSet<String>,
366 ctx: &crate::lint_context::LintContext,
367 mkdocs_mode: bool,
368 ) -> Vec<(usize, usize, usize, String)> {
369 let mut undefined = Vec::new();
370 let mut reported_refs = HashMap::new();
371 let mut in_code_block = false;
372 let mut code_fence_marker = String::new();
373 let mut in_example_section = false;
374
375 let code_spans = ctx.code_spans();
377
378 for link in &ctx.links {
380 if !link.is_reference {
381 continue; }
383
384 if ctx.is_in_jinja_range(link.byte_offset) {
386 continue;
387 }
388
389 if Self::is_in_code_span(link.line, link.start_col, &code_spans) {
391 continue;
392 }
393
394 if Self::is_in_html_comment(content, link.byte_offset) {
396 continue;
397 }
398
399 if Self::is_in_html_tag(ctx, link.byte_offset) {
401 continue;
402 }
403
404 if is_in_math_context(ctx, link.byte_offset) {
406 continue;
407 }
408
409 if is_in_table_cell(ctx, link.line, link.start_col) {
411 continue;
412 }
413
414 if ctx.line_info(link.line).is_some_and(|info| info.in_front_matter) {
416 continue;
417 }
418
419 if let Some(ref_id) = &link.reference_id {
420 let reference_lower = ref_id.to_lowercase();
421
422 if self.is_known_non_reference_pattern(ref_id) {
424 continue;
425 }
426
427 let stripped_ref = Self::strip_backticks(ref_id);
431 let stripped_text = Self::strip_backticks(&link.text);
432 if mkdocs_mode
433 && (is_mkdocs_auto_reference(stripped_ref)
434 || is_mkdocs_auto_reference(stripped_text)
435 || (ref_id != stripped_ref && Self::is_valid_python_identifier(stripped_ref))
436 || (link.text.as_ref() != stripped_text && Self::is_valid_python_identifier(stripped_text)))
437 {
438 continue;
439 }
440
441 if !references.contains(&reference_lower) && !reported_refs.contains_key(&reference_lower) {
443 if let Some(line_info) = ctx.line_info(link.line) {
445 if OUTPUT_EXAMPLE_START.is_match(line_info.content(ctx.content)) {
446 in_example_section = true;
447 continue;
448 }
449
450 if in_example_section {
451 continue;
452 }
453
454 if LIST_ITEM_REGEX.is_match(line_info.content(ctx.content)) {
456 continue;
457 }
458
459 let trimmed = line_info.content(ctx.content).trim_start();
461 if trimmed.starts_with('<') {
462 continue;
463 }
464 }
465
466 let match_len = link.byte_end - link.byte_offset;
467 undefined.push((link.line - 1, link.start_col, match_len, ref_id.to_string()));
468 reported_refs.insert(reference_lower, true);
469 }
470 }
471 }
472
473 for image in &ctx.images {
475 if !image.is_reference {
476 continue; }
478
479 if ctx.is_in_jinja_range(image.byte_offset) {
481 continue;
482 }
483
484 if Self::is_in_code_span(image.line, image.start_col, &code_spans) {
486 continue;
487 }
488
489 if Self::is_in_html_comment(content, image.byte_offset) {
491 continue;
492 }
493
494 if Self::is_in_html_tag(ctx, image.byte_offset) {
496 continue;
497 }
498
499 if is_in_math_context(ctx, image.byte_offset) {
501 continue;
502 }
503
504 if is_in_table_cell(ctx, image.line, image.start_col) {
506 continue;
507 }
508
509 if ctx.line_info(image.line).is_some_and(|info| info.in_front_matter) {
511 continue;
512 }
513
514 if let Some(ref_id) = &image.reference_id {
515 let reference_lower = ref_id.to_lowercase();
516
517 if self.is_known_non_reference_pattern(ref_id) {
519 continue;
520 }
521
522 let stripped_ref = Self::strip_backticks(ref_id);
526 let stripped_alt = Self::strip_backticks(&image.alt_text);
527 if mkdocs_mode
528 && (is_mkdocs_auto_reference(stripped_ref)
529 || is_mkdocs_auto_reference(stripped_alt)
530 || (ref_id != stripped_ref && Self::is_valid_python_identifier(stripped_ref))
531 || (image.alt_text.as_ref() != stripped_alt && Self::is_valid_python_identifier(stripped_alt)))
532 {
533 continue;
534 }
535
536 if !references.contains(&reference_lower) && !reported_refs.contains_key(&reference_lower) {
538 if let Some(line_info) = ctx.line_info(image.line) {
540 if OUTPUT_EXAMPLE_START.is_match(line_info.content(ctx.content)) {
541 in_example_section = true;
542 continue;
543 }
544
545 if in_example_section {
546 continue;
547 }
548
549 if LIST_ITEM_REGEX.is_match(line_info.content(ctx.content)) {
551 continue;
552 }
553
554 let trimmed = line_info.content(ctx.content).trim_start();
556 if trimmed.starts_with('<') {
557 continue;
558 }
559 }
560
561 let match_len = image.byte_end - image.byte_offset;
562 undefined.push((image.line - 1, image.start_col, match_len, ref_id.to_string()));
563 reported_refs.insert(reference_lower, true);
564 }
565 }
566 }
567
568 let mut covered_ranges: Vec<(usize, usize)> = Vec::new();
570
571 for link in &ctx.links {
573 covered_ranges.push((link.byte_offset, link.byte_end));
574 }
575
576 for image in &ctx.images {
578 covered_ranges.push((image.byte_offset, image.byte_end));
579 }
580
581 covered_ranges.sort_by_key(|&(start, _)| start);
583
584 if !self.config.shortcut_syntax {
589 return undefined;
590 }
591
592 let lines: Vec<&str> = content.lines().collect();
594 in_example_section = false; for (line_num, line) in lines.iter().enumerate() {
597 if ctx.line_info(line_num + 1).is_some_and(|info| info.in_front_matter) {
599 continue;
600 }
601
602 if let Some(cap) = FENCED_CODE_START.captures(line) {
604 if let Some(fence) = cap.get(2) {
605 let fence_str = fence.as_str();
607 if !in_code_block {
608 in_code_block = true;
609 code_fence_marker = fence_str.to_string();
610 } else if line.trim_start().starts_with(&code_fence_marker) {
611 let trimmed = line.trim_start();
613 if trimmed.starts_with(&code_fence_marker) {
615 let after_fence = &trimmed[code_fence_marker.len()..];
616 if after_fence.trim().is_empty() {
617 in_code_block = false;
618 code_fence_marker.clear();
619 }
620 }
621 }
622 }
623 continue;
624 }
625
626 if in_code_block {
627 continue;
628 }
629
630 if OUTPUT_EXAMPLE_START.is_match(line) {
632 in_example_section = true;
633 continue;
634 }
635
636 if in_example_section {
637 if line.starts_with('#') && !OUTPUT_EXAMPLE_START.is_match(line) {
639 in_example_section = false;
640 } else {
641 continue;
642 }
643 }
644
645 if LIST_ITEM_REGEX.is_match(line) {
647 continue;
648 }
649
650 let trimmed_line = line.trim_start();
652 if trimmed_line.starts_with('<') {
653 continue;
654 }
655
656 if GITHUB_ALERT_REGEX.is_match(line) {
658 continue;
659 }
660
661 if trimmed_line.starts_with("*[") {
664 continue;
665 }
666
667 let mut url_bracket_ranges: Vec<(usize, usize)> = Vec::new();
670 for mat in URL_WITH_BRACKETS.find_iter(line) {
671 let url_str = mat.as_str();
673 let url_start = mat.start();
674
675 let mut idx = 0;
677 while idx < url_str.len() {
678 if let Some(bracket_start) = url_str[idx..].find('[') {
679 let bracket_start_abs = url_start + idx + bracket_start;
680 if let Some(bracket_end) = url_str[idx + bracket_start + 1..].find(']') {
681 let bracket_end_abs = url_start + idx + bracket_start + 1 + bracket_end + 1;
682 url_bracket_ranges.push((bracket_start_abs, bracket_end_abs));
683 idx += bracket_start + bracket_end + 2;
684 } else {
685 break;
686 }
687 } else {
688 break;
689 }
690 }
691 }
692
693 if let Ok(captures) = SHORTCUT_REF_REGEX.captures_iter(line).collect::<Result<Vec<_>, _>>() {
695 for cap in captures {
696 if let Some(ref_match) = cap.get(1) {
697 let bracket_start = cap.get(0).unwrap().start();
699 let bracket_end = cap.get(0).unwrap().end();
700
701 let is_in_url = url_bracket_ranges
703 .iter()
704 .any(|&(url_start, url_end)| bracket_start >= url_start && bracket_end <= url_end);
705
706 if is_in_url {
707 continue;
708 }
709
710 if bracket_start > 0 {
713 if let Some(byte) = line.as_bytes().get(bracket_start.saturating_sub(1))
715 && *byte == b'^'
716 {
717 continue; }
719 }
720
721 let reference = ref_match.as_str();
722 let reference_lower = reference.to_lowercase();
723
724 if self.is_known_non_reference_pattern(reference) {
726 continue;
727 }
728
729 if let Some(alert_type) = reference.strip_prefix('!')
731 && matches!(
732 alert_type,
733 "NOTE"
734 | "TIP"
735 | "WARNING"
736 | "IMPORTANT"
737 | "CAUTION"
738 | "INFO"
739 | "SUCCESS"
740 | "FAILURE"
741 | "DANGER"
742 | "BUG"
743 | "EXAMPLE"
744 | "QUOTE"
745 )
746 {
747 continue;
748 }
749
750 if mkdocs_mode
753 && (reference.starts_with("start:") || reference.starts_with("end:"))
754 && (crate::utils::mkdocs_snippets::is_snippet_section_start(line)
755 || crate::utils::mkdocs_snippets::is_snippet_section_end(line))
756 {
757 continue;
758 }
759
760 let stripped_ref = Self::strip_backticks(reference);
763 if mkdocs_mode
764 && (is_mkdocs_auto_reference(stripped_ref)
765 || (reference != stripped_ref && Self::is_valid_python_identifier(stripped_ref)))
766 {
767 continue;
768 }
769
770 if !references.contains(&reference_lower) && !reported_refs.contains_key(&reference_lower) {
771 let full_match = cap.get(0).unwrap();
772 let col = full_match.start();
773
774 let code_spans = ctx.code_spans();
776 if Self::is_in_code_span(line_num + 1, col, &code_spans) {
777 continue;
778 }
779
780 let line_start_byte = ctx.line_offsets[line_num];
782 let byte_pos = line_start_byte + col;
783
784 if ctx.is_in_jinja_range(byte_pos) {
786 continue;
787 }
788
789 if crate::utils::code_block_utils::CodeBlockUtils::is_in_code_block(
791 &ctx.code_blocks,
792 byte_pos,
793 ) {
794 continue;
795 }
796
797 if Self::is_in_html_comment(content, byte_pos) {
799 continue;
800 }
801
802 if Self::is_in_html_tag(ctx, byte_pos) {
804 continue;
805 }
806
807 if is_in_math_context(ctx, byte_pos) {
809 continue;
810 }
811
812 if is_in_table_cell(ctx, line_num + 1, col) {
814 continue;
815 }
816
817 let byte_end = byte_pos + (full_match.end() - full_match.start());
818
819 let mut is_covered = false;
821 for &(range_start, range_end) in &covered_ranges {
822 if range_start <= byte_pos && byte_end <= range_end {
823 is_covered = true;
825 break;
826 }
827 if range_start > byte_end {
828 break;
830 }
831 }
832
833 if is_covered {
834 continue;
835 }
836
837 let line_chars: Vec<char> = line.chars().collect();
842 if col > 0 && col <= line_chars.len() && line_chars.get(col - 1) == Some(&']') {
843 let mut bracket_count = 1; let mut check_pos = col.saturating_sub(2);
846 let mut found_opening = false;
847
848 while check_pos > 0 && check_pos < line_chars.len() {
849 match line_chars.get(check_pos) {
850 Some(&']') => bracket_count += 1,
851 Some(&'[') => {
852 bracket_count -= 1;
853 if bracket_count == 0 {
854 if check_pos == 0 || line_chars.get(check_pos - 1) != Some(&'\\') {
856 found_opening = true;
857 }
858 break;
859 }
860 }
861 _ => {}
862 }
863 if check_pos == 0 {
864 break;
865 }
866 check_pos = check_pos.saturating_sub(1);
867 }
868
869 if found_opening {
870 continue;
872 }
873 }
874
875 let before_text = &line[..col];
878 if before_text.contains("\\]") {
879 if let Some(escaped_close_pos) = before_text.rfind("\\]") {
881 let search_text = &before_text[..escaped_close_pos];
882 if search_text.contains("\\[") {
883 continue;
885 }
886 }
887 }
888
889 let match_len = full_match.end() - full_match.start();
890 undefined.push((line_num, col, match_len, reference.to_string()));
891 reported_refs.insert(reference_lower, true);
892 }
893 }
894 }
895 }
896 }
897
898 undefined
899 }
900}
901
902impl Rule for MD052ReferenceLinkImages {
903 fn name(&self) -> &'static str {
904 "MD052"
905 }
906
907 fn description(&self) -> &'static str {
908 "Reference links and images should use a reference that exists"
909 }
910
911 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
912 let content = ctx.content;
913 let mut warnings = Vec::new();
914
915 if !content.contains('[') {
917 return Ok(warnings);
918 }
919
920 let mkdocs_mode = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
922
923 let references = self.extract_references(content, mkdocs_mode);
924
925 for (line_num, col, match_len, reference) in
927 self.find_undefined_references(content, &references, ctx, mkdocs_mode)
928 {
929 let lines: Vec<&str> = content.lines().collect();
930 let line_content = lines.get(line_num).unwrap_or(&"");
931
932 let (start_line, start_col, end_line, end_col) =
934 calculate_match_range(line_num + 1, line_content, col, match_len);
935
936 warnings.push(LintWarning {
937 rule_name: Some(self.name().to_string()),
938 line: start_line,
939 column: start_col,
940 end_line,
941 end_column: end_col,
942 message: format!("Reference '{reference}' not found"),
943 severity: Severity::Warning,
944 fix: None,
945 });
946 }
947
948 Ok(warnings)
949 }
950
951 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
953 ctx.content.is_empty() || !ctx.likely_has_links_or_images()
955 }
956
957 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
958 let content = ctx.content;
959 Ok(content.to_string())
961 }
962
963 fn as_any(&self) -> &dyn std::any::Any {
964 self
965 }
966
967 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
968 where
969 Self: Sized,
970 {
971 let rule_config = crate::rule_config_serde::load_rule_config::<MD052Config>(config);
972 Box::new(Self::from_config_struct(rule_config))
973 }
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979 use crate::lint_context::LintContext;
980
981 #[test]
982 fn test_valid_reference_link() {
983 let rule = MD052ReferenceLinkImages::new();
984 let content = "[text][ref]\n\n[ref]: https://example.com";
985 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
986 let result = rule.check(&ctx).unwrap();
987
988 assert_eq!(result.len(), 0);
989 }
990
991 #[test]
992 fn test_undefined_reference_link() {
993 let rule = MD052ReferenceLinkImages::new();
994 let content = "[text][undefined]";
995 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
996 let result = rule.check(&ctx).unwrap();
997
998 assert_eq!(result.len(), 1);
999 assert!(result[0].message.contains("Reference 'undefined' not found"));
1000 }
1001
1002 #[test]
1003 fn test_valid_reference_image() {
1004 let rule = MD052ReferenceLinkImages::new();
1005 let content = "![alt][img]\n\n[img]: image.jpg";
1006 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1007 let result = rule.check(&ctx).unwrap();
1008
1009 assert_eq!(result.len(), 0);
1010 }
1011
1012 #[test]
1013 fn test_undefined_reference_image() {
1014 let rule = MD052ReferenceLinkImages::new();
1015 let content = "![alt][missing]";
1016 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1017 let result = rule.check(&ctx).unwrap();
1018
1019 assert_eq!(result.len(), 1);
1020 assert!(result[0].message.contains("Reference 'missing' not found"));
1021 }
1022
1023 #[test]
1024 fn test_case_insensitive_references() {
1025 let rule = MD052ReferenceLinkImages::new();
1026 let content = "[Text][REF]\n\n[ref]: https://example.com";
1027 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1028 let result = rule.check(&ctx).unwrap();
1029
1030 assert_eq!(result.len(), 0);
1031 }
1032
1033 #[test]
1034 fn test_shortcut_reference_valid() {
1035 let rule = MD052ReferenceLinkImages::new();
1036 let content = "[ref]\n\n[ref]: https://example.com";
1037 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1038 let result = rule.check(&ctx).unwrap();
1039
1040 assert_eq!(result.len(), 0);
1041 }
1042
1043 #[test]
1044 fn test_shortcut_reference_undefined_with_shortcut_syntax_enabled() {
1045 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1048 shortcut_syntax: true,
1049 ..Default::default()
1050 });
1051 let content = "[undefined]";
1052 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1053 let result = rule.check(&ctx).unwrap();
1054
1055 assert_eq!(result.len(), 1);
1056 assert!(result[0].message.contains("Reference 'undefined' not found"));
1057 }
1058
1059 #[test]
1060 fn test_shortcut_reference_not_checked_by_default() {
1061 let rule = MD052ReferenceLinkImages::new();
1063 let content = "[undefined]";
1064 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1065 let result = rule.check(&ctx).unwrap();
1066
1067 assert_eq!(result.len(), 0);
1069 }
1070
1071 #[test]
1072 fn test_inline_links_ignored() {
1073 let rule = MD052ReferenceLinkImages::new();
1074 let content = "[text](https://example.com)";
1075 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1076 let result = rule.check(&ctx).unwrap();
1077
1078 assert_eq!(result.len(), 0);
1079 }
1080
1081 #[test]
1082 fn test_inline_images_ignored() {
1083 let rule = MD052ReferenceLinkImages::new();
1084 let content = "";
1085 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1086 let result = rule.check(&ctx).unwrap();
1087
1088 assert_eq!(result.len(), 0);
1089 }
1090
1091 #[test]
1092 fn test_references_in_code_blocks_ignored() {
1093 let rule = MD052ReferenceLinkImages::new();
1094 let content = "```\n[undefined]\n```\n\n[ref]: https://example.com";
1095 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1096 let result = rule.check(&ctx).unwrap();
1097
1098 assert_eq!(result.len(), 0);
1099 }
1100
1101 #[test]
1102 fn test_references_in_inline_code_ignored() {
1103 let rule = MD052ReferenceLinkImages::new();
1104 let content = "`[undefined]`";
1105 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1106 let result = rule.check(&ctx).unwrap();
1107
1108 assert_eq!(result.len(), 0);
1110 }
1111
1112 #[test]
1113 fn test_comprehensive_inline_code_detection() {
1114 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1116 shortcut_syntax: true,
1117 ..Default::default()
1118 });
1119 let content = r#"# Test
1120
1121This `[inside]` should be ignored.
1122This [outside] should be flagged.
1123Reference links `[text][ref]` in code are ignored.
1124Regular reference [text][missing] should be flagged.
1125Images `![alt][img]` in code are ignored.
1126Regular image ![alt][badimg] should be flagged.
1127
1128Multiple `[one]` and `[two]` in code ignored, but [three] is not.
1129
1130```
1131[code block content] should be ignored
1132```
1133
1134`Multiple [refs] in [same] code span` ignored."#;
1135
1136 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1137 let result = rule.check(&ctx).unwrap();
1138
1139 assert_eq!(result.len(), 4);
1141
1142 let messages: Vec<&str> = result.iter().map(|w| &*w.message).collect();
1143 assert!(messages.iter().any(|m| m.contains("outside")));
1144 assert!(messages.iter().any(|m| m.contains("missing")));
1145 assert!(messages.iter().any(|m| m.contains("badimg")));
1146 assert!(messages.iter().any(|m| m.contains("three")));
1147
1148 assert!(!messages.iter().any(|m| m.contains("inside")));
1150 assert!(!messages.iter().any(|m| m.contains("one")));
1151 assert!(!messages.iter().any(|m| m.contains("two")));
1152 assert!(!messages.iter().any(|m| m.contains("refs")));
1153 assert!(!messages.iter().any(|m| m.contains("same")));
1154 }
1155
1156 #[test]
1157 fn test_multiple_undefined_references() {
1158 let rule = MD052ReferenceLinkImages::new();
1159 let content = "[link1][ref1] [link2][ref2] [link3][ref3]";
1160 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1161 let result = rule.check(&ctx).unwrap();
1162
1163 assert_eq!(result.len(), 3);
1164 assert!(result[0].message.contains("ref1"));
1165 assert!(result[1].message.contains("ref2"));
1166 assert!(result[2].message.contains("ref3"));
1167 }
1168
1169 #[test]
1170 fn test_mixed_valid_and_undefined() {
1171 let rule = MD052ReferenceLinkImages::new();
1172 let content = "[valid][ref] [invalid][missing]\n\n[ref]: https://example.com";
1173 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1174 let result = rule.check(&ctx).unwrap();
1175
1176 assert_eq!(result.len(), 1);
1177 assert!(result[0].message.contains("missing"));
1178 }
1179
1180 #[test]
1181 fn test_empty_reference() {
1182 let rule = MD052ReferenceLinkImages::new();
1183 let content = "[text][]\n\n[ref]: https://example.com";
1184 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1185 let result = rule.check(&ctx).unwrap();
1186
1187 assert_eq!(result.len(), 1);
1189 }
1190
1191 #[test]
1192 fn test_escaped_brackets_ignored() {
1193 let rule = MD052ReferenceLinkImages::new();
1194 let content = "\\[not a link\\]";
1195 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1196 let result = rule.check(&ctx).unwrap();
1197
1198 assert_eq!(result.len(), 0);
1199 }
1200
1201 #[test]
1202 fn test_list_items_ignored() {
1203 let rule = MD052ReferenceLinkImages::new();
1204 let content = "- [undefined]\n* [another]\n+ [third]";
1205 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1206 let result = rule.check(&ctx).unwrap();
1207
1208 assert_eq!(result.len(), 0);
1210 }
1211
1212 #[test]
1213 fn test_output_example_section_ignored() {
1214 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1216 shortcut_syntax: true,
1217 ..Default::default()
1218 });
1219 let content = "## Output\n\n[undefined]\n\n## Normal Section\n\n[missing]";
1220 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1221 let result = rule.check(&ctx).unwrap();
1222
1223 assert_eq!(result.len(), 1);
1225 assert!(result[0].message.contains("missing"));
1226 }
1227
1228 #[test]
1229 fn test_reference_definitions_in_code_blocks_ignored() {
1230 let rule = MD052ReferenceLinkImages::new();
1231 let content = "[link][ref]\n\n```\n[ref]: https://example.com\n```";
1232 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1233 let result = rule.check(&ctx).unwrap();
1234
1235 assert_eq!(result.len(), 1);
1237 assert!(result[0].message.contains("ref"));
1238 }
1239
1240 #[test]
1241 fn test_multiple_references_to_same_undefined() {
1242 let rule = MD052ReferenceLinkImages::new();
1243 let content = "[first][missing] [second][missing] [third][missing]";
1244 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1245 let result = rule.check(&ctx).unwrap();
1246
1247 assert_eq!(result.len(), 1);
1249 assert!(result[0].message.contains("missing"));
1250 }
1251
1252 #[test]
1253 fn test_reference_with_special_characters() {
1254 let rule = MD052ReferenceLinkImages::new();
1255 let content = "[text][ref-with-hyphens]\n\n[ref-with-hyphens]: https://example.com";
1256 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1257 let result = rule.check(&ctx).unwrap();
1258
1259 assert_eq!(result.len(), 0);
1260 }
1261
1262 #[test]
1263 fn test_issue_51_html_attribute_not_reference() {
1264 let rule = MD052ReferenceLinkImages::new();
1266 let content = r#"# Example
1267
1268## Test
1269
1270Want to fill out this form?
1271
1272<form method="post">
1273 <input type="email" name="fields[email]" id="drip-email" placeholder="email@domain.com">
1274</form>"#;
1275 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1276 let result = rule.check(&ctx).unwrap();
1277
1278 assert_eq!(
1279 result.len(),
1280 0,
1281 "HTML attributes with square brackets should not be flagged as undefined references"
1282 );
1283 }
1284
1285 #[test]
1286 fn test_extract_references() {
1287 let rule = MD052ReferenceLinkImages::new();
1288 let content = "[ref1]: url1\n[Ref2]: url2\n[REF3]: url3";
1289 let refs = rule.extract_references(content, false);
1290
1291 assert_eq!(refs.len(), 3);
1292 assert!(refs.contains("ref1"));
1293 assert!(refs.contains("ref2"));
1294 assert!(refs.contains("ref3"));
1295 }
1296
1297 #[test]
1298 fn test_inline_code_not_flagged() {
1299 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1301 shortcut_syntax: true,
1302 ..Default::default()
1303 });
1304
1305 let content = r#"# Test
1307
1308Configure with `["JavaScript", "GitHub", "Node.js"]` in your settings.
1309
1310Also, `[todo]` is not a reference link.
1311
1312But this [reference] should be flagged.
1313
1314And this `[inline code]` should not be flagged.
1315"#;
1316
1317 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1318 let warnings = rule.check(&ctx).unwrap();
1319
1320 assert_eq!(warnings.len(), 1, "Should only flag one undefined reference");
1322 assert!(warnings[0].message.contains("'reference'"));
1323 }
1324
1325 #[test]
1326 fn test_code_block_references_ignored() {
1327 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1329 shortcut_syntax: true,
1330 ..Default::default()
1331 });
1332
1333 let content = r#"# Test
1334
1335```markdown
1336[undefined] reference in code block
1337![undefined] image in code block
1338```
1339
1340[real-undefined] reference outside
1341"#;
1342
1343 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1344 let warnings = rule.check(&ctx).unwrap();
1345
1346 assert_eq!(warnings.len(), 1);
1348 assert!(warnings[0].message.contains("'real-undefined'"));
1349 }
1350
1351 #[test]
1352 fn test_html_comments_ignored() {
1353 let rule = MD052ReferenceLinkImages::new();
1355
1356 let content = r#"<!--- write fake_editor.py 'import sys\nopen(*sys.argv[1:], mode="wt").write("2 3 4 4 2 3 2")' -->
1358<!--- set_env EDITOR 'python3 fake_editor.py' -->
1359
1360```bash
1361$ python3 vote.py
13623 votes for: 2
13632 votes for: 3, 4
1364```"#;
1365 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1366 let result = rule.check(&ctx).unwrap();
1367 assert_eq!(result.len(), 0, "Should not flag [1:] inside HTML comments");
1368
1369 let content = r#"<!-- This is [ref1] and [ref2][ref3] -->
1371Normal [text][undefined]
1372<!-- Another [comment][with] references -->"#;
1373 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1374 let result = rule.check(&ctx).unwrap();
1375 assert_eq!(
1376 result.len(),
1377 1,
1378 "Should only flag the undefined reference outside comments"
1379 );
1380 assert!(result[0].message.contains("undefined"));
1381
1382 let content = r#"<!--
1384[ref1]
1385[ref2][ref3]
1386-->
1387[actual][undefined]"#;
1388 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1389 let result = rule.check(&ctx).unwrap();
1390 assert_eq!(
1391 result.len(),
1392 1,
1393 "Should not flag references in multi-line HTML comments"
1394 );
1395 assert!(result[0].message.contains("undefined"));
1396
1397 let content = r#"<!-- Comment with [1:] pattern -->
1399Valid [link][ref]
1400<!-- More [refs][in][comments] -->
1401![image][missing]
1402
1403[ref]: https://example.com"#;
1404 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1405 let result = rule.check(&ctx).unwrap();
1406 assert_eq!(result.len(), 1, "Should only flag missing image reference");
1407 assert!(result[0].message.contains("missing"));
1408 }
1409
1410 #[test]
1411 fn test_frontmatter_ignored() {
1412 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1415 shortcut_syntax: true,
1416 ..Default::default()
1417 });
1418
1419 let content = r#"---
1421layout: post
1422title: "My Jekyll Post"
1423date: 2023-01-01
1424categories: blog
1425tags: ["test", "example"]
1426author: John Doe
1427---
1428
1429# My Blog Post
1430
1431This is the actual markdown content that should be linted.
1432
1433[undefined] reference should be flagged.
1434
1435## Section 1
1436
1437Some content here."#;
1438 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1439 let result = rule.check(&ctx).unwrap();
1440
1441 assert_eq!(
1443 result.len(),
1444 1,
1445 "Should only flag the undefined reference outside frontmatter"
1446 );
1447 assert!(result[0].message.contains("undefined"));
1448
1449 let content = r#"+++
1451title = "My Post"
1452tags = ["example", "test"]
1453+++
1454
1455# Content
1456
1457[missing] reference should be flagged."#;
1458 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1459 let result = rule.check(&ctx).unwrap();
1460 assert_eq!(
1461 result.len(),
1462 1,
1463 "Should only flag the undefined reference outside TOML frontmatter"
1464 );
1465 assert!(result[0].message.contains("missing"));
1466 }
1467
1468 #[test]
1469 fn test_mkdocs_snippet_markers_not_flagged() {
1470 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1473 shortcut_syntax: true,
1474 ..Default::default()
1475 });
1476
1477 let content = r#"# Document with MkDocs Snippets
1479
1480Some content here.
1481
1482# -8<- [start:remote-content]
1483
1484This is the remote content section.
1485
1486# -8<- [end:remote-content]
1487
1488More content here.
1489
1490<!-- --8<-- [start:another-section] -->
1491Content in another section
1492<!-- --8<-- [end:another-section] -->"#;
1493 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1494 let result = rule.check(&ctx).unwrap();
1495
1496 assert_eq!(
1498 result.len(),
1499 0,
1500 "Should not flag MkDocs snippet markers as undefined references"
1501 );
1502
1503 let content = r#"# Document
1506
1507# -8<- [start:section]
1508Content with [reference] inside snippet section
1509# -8<- [end:section]
1510
1511Regular [undefined] reference outside snippet markers."#;
1512 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1513 let result = rule.check(&ctx).unwrap();
1514
1515 assert_eq!(
1516 result.len(),
1517 2,
1518 "Should flag undefined references but skip snippet marker lines"
1519 );
1520 assert!(result[0].message.contains("reference"));
1522 assert!(result[1].message.contains("undefined"));
1523
1524 let content = r#"# Document
1526
1527# -8<- [start:section]
1528# -8<- [end:section]"#;
1529 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1530 let result = rule.check(&ctx).unwrap();
1531
1532 assert_eq!(
1533 result.len(),
1534 2,
1535 "In standard mode, snippet markers should be flagged as undefined references"
1536 );
1537 }
1538
1539 #[test]
1540 fn test_pandoc_citations_not_flagged() {
1541 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1544 shortcut_syntax: true,
1545 ..Default::default()
1546 });
1547
1548 let content = r#"# Research Paper
1549
1550We are using the **bookdown** package [@R-bookdown] in this sample book.
1551This was built on top of R Markdown and **knitr** [@xie2015].
1552
1553Multiple citations [@citation1; @citation2; @citation3] are also supported.
1554
1555Regular [undefined] reference should still be flagged.
1556"#;
1557 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1558 let result = rule.check(&ctx).unwrap();
1559
1560 assert_eq!(
1562 result.len(),
1563 1,
1564 "Should only flag the undefined reference, not Pandoc citations"
1565 );
1566 assert!(result[0].message.contains("undefined"));
1567 }
1568
1569 #[test]
1570 fn test_pandoc_inline_footnotes_not_flagged() {
1571 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1574 shortcut_syntax: true,
1575 ..Default::default()
1576 });
1577
1578 let content = r#"# Math Document
1579
1580You can use math in footnotes like this^[where we mention $p = \frac{a}{b}$].
1581
1582Another footnote^[with some text and a [link](https://example.com)].
1583
1584But this [reference] without ^ should be flagged.
1585"#;
1586 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1587 let result = rule.check(&ctx).unwrap();
1588
1589 assert_eq!(
1591 result.len(),
1592 1,
1593 "Should only flag the regular reference, not inline footnotes"
1594 );
1595 assert!(result[0].message.contains("reference"));
1596 }
1597
1598 #[test]
1599 fn test_github_alerts_not_flagged() {
1600 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1603 shortcut_syntax: true,
1604 ..Default::default()
1605 });
1606
1607 let content = r#"# Document with GitHub Alerts
1609
1610> [!NOTE]
1611> This is a note alert.
1612
1613> [!TIP]
1614> This is a tip alert.
1615
1616> [!IMPORTANT]
1617> This is an important alert.
1618
1619> [!WARNING]
1620> This is a warning alert.
1621
1622> [!CAUTION]
1623> This is a caution alert.
1624
1625Regular content with [undefined] reference."#;
1626 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1627 let result = rule.check(&ctx).unwrap();
1628
1629 assert_eq!(
1631 result.len(),
1632 1,
1633 "Should only flag the undefined reference, not GitHub alerts"
1634 );
1635 assert!(result[0].message.contains("undefined"));
1636 assert_eq!(result[0].line, 18); let content = r#"> [!TIP]
1640> Here's a useful tip about [something].
1641> Multiple lines are allowed.
1642
1643[something] is mentioned but not defined."#;
1644 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1645 let result = rule.check(&ctx).unwrap();
1646
1647 assert_eq!(result.len(), 1, "Should flag undefined reference");
1651 assert!(result[0].message.contains("something"));
1652
1653 let content = r#"> [!NOTE]
1655> See [reference] for more details.
1656
1657[reference]: https://example.com"#;
1658 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1659 let result = rule.check(&ctx).unwrap();
1660
1661 assert_eq!(result.len(), 0, "Should not flag GitHub alerts or defined references");
1663 }
1664
1665 #[test]
1666 fn test_ignore_config() {
1667 let config = MD052Config {
1669 shortcut_syntax: true,
1670 ignore: vec!["Vec".to_string(), "HashMap".to_string(), "Option".to_string()],
1671 };
1672 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1673
1674 let content = r#"# Document with Custom Types
1675
1676Use [Vec] for dynamic arrays.
1677Use [HashMap] for key-value storage.
1678Use [Option] for nullable values.
1679Use [Result] for error handling.
1680"#;
1681 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1682 let result = rule.check(&ctx).unwrap();
1683
1684 assert_eq!(result.len(), 1, "Should only flag names not in ignore");
1686 assert!(result[0].message.contains("Result"));
1687 }
1688
1689 #[test]
1690 fn test_ignore_case_insensitive() {
1691 let config = MD052Config {
1693 shortcut_syntax: true,
1694 ignore: vec!["Vec".to_string()],
1695 };
1696 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1697
1698 let content = r#"# Case Insensitivity Test
1699
1700[Vec] should be ignored.
1701[vec] should also be ignored (different case, same match).
1702[VEC] should also be ignored (different case, same match).
1703[undefined] should be flagged (not in ignore list).
1704"#;
1705 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1706 let result = rule.check(&ctx).unwrap();
1707
1708 assert_eq!(result.len(), 1, "Should only flag non-ignored reference");
1710 assert!(result[0].message.contains("undefined"));
1711 }
1712
1713 #[test]
1714 fn test_ignore_empty_by_default() {
1715 let rule = MD052ReferenceLinkImages::new();
1717
1718 let content = "[text][undefined]";
1719 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1720 let result = rule.check(&ctx).unwrap();
1721
1722 assert_eq!(result.len(), 1);
1724 assert!(result[0].message.contains("undefined"));
1725 }
1726
1727 #[test]
1728 fn test_ignore_with_reference_links() {
1729 let config = MD052Config {
1731 shortcut_syntax: false,
1732 ignore: vec!["CustomType".to_string()],
1733 };
1734 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1735
1736 let content = r#"# Test
1737
1738See [documentation][CustomType] for details.
1739See [other docs][MissingRef] for more.
1740"#;
1741 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1742 let result = rule.check(&ctx).unwrap();
1743
1744 for (i, w) in result.iter().enumerate() {
1746 eprintln!("Warning {}: {}", i, w.message);
1747 }
1748
1749 assert_eq!(result.len(), 1, "Expected 1 warning, got {}", result.len());
1752 assert!(
1753 result[0].message.contains("missingref"),
1754 "Expected 'missingref' in message: {}",
1755 result[0].message
1756 );
1757 }
1758
1759 #[test]
1760 fn test_ignore_multiple() {
1761 let config = MD052Config {
1763 shortcut_syntax: true,
1764 ignore: vec![
1765 "i32".to_string(),
1766 "u64".to_string(),
1767 "String".to_string(),
1768 "Arc".to_string(),
1769 "Mutex".to_string(),
1770 ],
1771 };
1772 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1773
1774 let content = r#"# Types
1775
1776[i32] [u64] [String] [Arc] [Mutex] [Box]
1777"#;
1778 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1779 let result = rule.check(&ctx).unwrap();
1780
1781 assert_eq!(result.len(), 1);
1785 assert!(result[0].message.contains("Box"));
1786 }
1787}