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::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_tag(ctx: &crate::lint_context::LintContext, byte_pos: usize) -> bool {
277 for html_tag in ctx.html_tags().iter() {
279 if html_tag.byte_offset <= byte_pos && byte_pos < html_tag.byte_end {
280 return true;
281 }
282 }
283 false
284 }
285
286 fn extract_references(&self, content: &str, mkdocs_mode: bool) -> HashSet<String> {
287 use crate::config::MarkdownFlavor;
288 use crate::utils::skip_context::is_mkdocs_snippet_line;
289
290 let mut references = HashSet::new();
291 let mut in_code_block = false;
292 let mut code_fence_marker = String::new();
293
294 for line in content.lines() {
295 if is_mkdocs_snippet_line(
297 line,
298 if mkdocs_mode {
299 MarkdownFlavor::MkDocs
300 } else {
301 MarkdownFlavor::Standard
302 },
303 ) {
304 continue;
305 }
306 if let Some(cap) = FENCED_CODE_START.captures(line) {
308 if let Some(fence) = cap.get(2) {
309 let fence_str = fence.as_str();
311 if !in_code_block {
312 in_code_block = true;
313 code_fence_marker = fence_str.to_string();
314 } else if line.trim_start().starts_with(&code_fence_marker) {
315 let trimmed = line.trim_start();
317 if trimmed.starts_with(&code_fence_marker) {
319 let after_fence = &trimmed[code_fence_marker.len()..];
320 if after_fence.trim().is_empty() {
321 in_code_block = false;
322 code_fence_marker.clear();
323 }
324 }
325 }
326 }
327 continue;
328 }
329
330 if in_code_block {
332 continue;
333 }
334
335 if line.trim_start().starts_with("*[") {
338 continue;
339 }
340
341 if let Some(cap) = REF_REGEX.captures(line) {
342 if let Some(reference) = cap.get(1) {
344 references.insert(reference.as_str().to_lowercase());
345 }
346 }
347 }
348
349 references
350 }
351
352 fn find_undefined_references(
353 &self,
354 references: &HashSet<String>,
355 ctx: &crate::lint_context::LintContext,
356 mkdocs_mode: bool,
357 ) -> Vec<(usize, usize, usize, String)> {
358 let mut undefined = Vec::new();
359 let mut reported_refs = HashMap::new();
360 let mut in_code_block = false;
361 let mut code_fence_marker = String::new();
362 let mut in_example_section = false;
363
364 let code_spans = ctx.code_spans();
366
367 for link in &ctx.links {
369 if !link.is_reference {
370 continue; }
372
373 if ctx.is_in_jinja_range(link.byte_offset) {
375 continue;
376 }
377
378 if Self::is_in_code_span(link.line, link.start_col, &code_spans) {
380 continue;
381 }
382
383 if ctx.is_in_html_comment(link.byte_offset) {
385 continue;
386 }
387
388 if Self::is_in_html_tag(ctx, link.byte_offset) {
390 continue;
391 }
392
393 if is_in_math_context(ctx, link.byte_offset) {
395 continue;
396 }
397
398 if is_in_table_cell(ctx, link.line, link.start_col) {
400 continue;
401 }
402
403 if ctx.line_info(link.line).is_some_and(|info| info.in_front_matter) {
405 continue;
406 }
407
408 if ctx.flavor == crate::config::MarkdownFlavor::Quarto && ctx.is_in_citation(link.byte_offset) {
411 continue;
412 }
413
414 if ctx.is_in_shortcode(link.byte_offset) {
417 continue;
418 }
419
420 if let Some(ref_id) = &link.reference_id {
421 let reference_lower = ref_id.to_lowercase();
422
423 if self.is_known_non_reference_pattern(ref_id) {
425 continue;
426 }
427
428 let stripped_ref = Self::strip_backticks(ref_id);
432 let stripped_text = Self::strip_backticks(&link.text);
433 if mkdocs_mode
434 && (is_mkdocs_auto_reference(stripped_ref)
435 || is_mkdocs_auto_reference(stripped_text)
436 || (ref_id != stripped_ref && Self::is_valid_python_identifier(stripped_ref))
437 || (link.text.as_ref() != stripped_text && Self::is_valid_python_identifier(stripped_text)))
438 {
439 continue;
440 }
441
442 if !references.contains(&reference_lower) && !reported_refs.contains_key(&reference_lower) {
444 if let Some(line_info) = ctx.line_info(link.line) {
446 if OUTPUT_EXAMPLE_START.is_match(line_info.content(ctx.content)) {
447 in_example_section = true;
448 continue;
449 }
450
451 if in_example_section {
452 continue;
453 }
454
455 if LIST_ITEM_REGEX.is_match(line_info.content(ctx.content)) {
457 continue;
458 }
459
460 let trimmed = line_info.content(ctx.content).trim_start();
462 if trimmed.starts_with('<') {
463 continue;
464 }
465 }
466
467 let match_len = link.byte_end - link.byte_offset;
468 undefined.push((link.line - 1, link.start_col, match_len, ref_id.to_string()));
469 reported_refs.insert(reference_lower, true);
470 }
471 }
472 }
473
474 for image in &ctx.images {
476 if !image.is_reference {
477 continue; }
479
480 if ctx.is_in_jinja_range(image.byte_offset) {
482 continue;
483 }
484
485 if Self::is_in_code_span(image.line, image.start_col, &code_spans) {
487 continue;
488 }
489
490 if ctx.is_in_html_comment(image.byte_offset) {
492 continue;
493 }
494
495 if Self::is_in_html_tag(ctx, image.byte_offset) {
497 continue;
498 }
499
500 if is_in_math_context(ctx, image.byte_offset) {
502 continue;
503 }
504
505 if is_in_table_cell(ctx, image.line, image.start_col) {
507 continue;
508 }
509
510 if ctx.line_info(image.line).is_some_and(|info| info.in_front_matter) {
512 continue;
513 }
514
515 if let Some(ref_id) = &image.reference_id {
516 let reference_lower = ref_id.to_lowercase();
517
518 if self.is_known_non_reference_pattern(ref_id) {
520 continue;
521 }
522
523 let stripped_ref = Self::strip_backticks(ref_id);
527 let stripped_alt = Self::strip_backticks(&image.alt_text);
528 if mkdocs_mode
529 && (is_mkdocs_auto_reference(stripped_ref)
530 || is_mkdocs_auto_reference(stripped_alt)
531 || (ref_id != stripped_ref && Self::is_valid_python_identifier(stripped_ref))
532 || (image.alt_text.as_ref() != stripped_alt && Self::is_valid_python_identifier(stripped_alt)))
533 {
534 continue;
535 }
536
537 if !references.contains(&reference_lower) && !reported_refs.contains_key(&reference_lower) {
539 if let Some(line_info) = ctx.line_info(image.line) {
541 if OUTPUT_EXAMPLE_START.is_match(line_info.content(ctx.content)) {
542 in_example_section = true;
543 continue;
544 }
545
546 if in_example_section {
547 continue;
548 }
549
550 if LIST_ITEM_REGEX.is_match(line_info.content(ctx.content)) {
552 continue;
553 }
554
555 let trimmed = line_info.content(ctx.content).trim_start();
557 if trimmed.starts_with('<') {
558 continue;
559 }
560 }
561
562 let match_len = image.byte_end - image.byte_offset;
563 undefined.push((image.line - 1, image.start_col, match_len, ref_id.to_string()));
564 reported_refs.insert(reference_lower, true);
565 }
566 }
567 }
568
569 let mut covered_ranges: Vec<(usize, usize)> = Vec::new();
571
572 for link in &ctx.links {
574 covered_ranges.push((link.byte_offset, link.byte_end));
575 }
576
577 for image in &ctx.images {
579 covered_ranges.push((image.byte_offset, image.byte_end));
580 }
581
582 covered_ranges.sort_by_key(|&(start, _)| start);
584
585 if !self.config.shortcut_syntax {
590 return undefined;
591 }
592
593 let lines = ctx.raw_lines();
595 in_example_section = false; for (line_num, line) in lines.iter().enumerate() {
598 if ctx.line_info(line_num + 1).is_some_and(|info| info.in_front_matter) {
600 continue;
601 }
602
603 if let Some(cap) = FENCED_CODE_START.captures(line) {
605 if let Some(fence) = cap.get(2) {
606 let fence_str = fence.as_str();
608 if !in_code_block {
609 in_code_block = true;
610 code_fence_marker = fence_str.to_string();
611 } else if line.trim_start().starts_with(&code_fence_marker) {
612 let trimmed = line.trim_start();
614 if trimmed.starts_with(&code_fence_marker) {
616 let after_fence = &trimmed[code_fence_marker.len()..];
617 if after_fence.trim().is_empty() {
618 in_code_block = false;
619 code_fence_marker.clear();
620 }
621 }
622 }
623 }
624 continue;
625 }
626
627 if in_code_block {
628 continue;
629 }
630
631 if OUTPUT_EXAMPLE_START.is_match(line) {
633 in_example_section = true;
634 continue;
635 }
636
637 if in_example_section {
638 if line.starts_with('#') && !OUTPUT_EXAMPLE_START.is_match(line) {
640 in_example_section = false;
641 } else {
642 continue;
643 }
644 }
645
646 if LIST_ITEM_REGEX.is_match(line) {
648 continue;
649 }
650
651 let trimmed_line = line.trim_start();
653 if trimmed_line.starts_with('<') {
654 continue;
655 }
656
657 if GITHUB_ALERT_REGEX.is_match(line) {
659 continue;
660 }
661
662 if trimmed_line.starts_with("*[") {
665 continue;
666 }
667
668 let mut url_bracket_ranges: Vec<(usize, usize)> = Vec::new();
671 for mat in URL_WITH_BRACKETS.find_iter(line) {
672 let url_str = mat.as_str();
674 let url_start = mat.start();
675
676 let mut idx = 0;
678 while idx < url_str.len() {
679 if let Some(bracket_start) = url_str[idx..].find('[') {
680 let bracket_start_abs = url_start + idx + bracket_start;
681 if let Some(bracket_end) = url_str[idx + bracket_start + 1..].find(']') {
682 let bracket_end_abs = url_start + idx + bracket_start + 1 + bracket_end + 1;
683 url_bracket_ranges.push((bracket_start_abs, bracket_end_abs));
684 idx += bracket_start + bracket_end + 2;
685 } else {
686 break;
687 }
688 } else {
689 break;
690 }
691 }
692 }
693
694 if let Ok(captures) = SHORTCUT_REF_REGEX.captures_iter(line).collect::<Result<Vec<_>, _>>() {
696 for cap in captures {
697 if let Some(ref_match) = cap.get(1) {
698 let bracket_start = cap.get(0).unwrap().start();
700 let bracket_end = cap.get(0).unwrap().end();
701
702 let is_in_url = url_bracket_ranges
704 .iter()
705 .any(|&(url_start, url_end)| bracket_start >= url_start && bracket_end <= url_end);
706
707 if is_in_url {
708 continue;
709 }
710
711 if bracket_start > 0 {
714 if let Some(byte) = line.as_bytes().get(bracket_start.saturating_sub(1))
716 && *byte == b'^'
717 {
718 continue; }
720 }
721
722 let reference = ref_match.as_str();
723 let reference_lower = reference.to_lowercase();
724
725 if self.is_known_non_reference_pattern(reference) {
727 continue;
728 }
729
730 if let Some(alert_type) = reference.strip_prefix('!')
732 && matches!(
733 alert_type,
734 "NOTE"
735 | "TIP"
736 | "WARNING"
737 | "IMPORTANT"
738 | "CAUTION"
739 | "INFO"
740 | "SUCCESS"
741 | "FAILURE"
742 | "DANGER"
743 | "BUG"
744 | "EXAMPLE"
745 | "QUOTE"
746 )
747 {
748 continue;
749 }
750
751 if mkdocs_mode
754 && (reference.starts_with("start:") || reference.starts_with("end:"))
755 && (crate::utils::mkdocs_snippets::is_snippet_section_start(line)
756 || crate::utils::mkdocs_snippets::is_snippet_section_end(line))
757 {
758 continue;
759 }
760
761 let stripped_ref = Self::strip_backticks(reference);
764 if mkdocs_mode
765 && (is_mkdocs_auto_reference(stripped_ref)
766 || (reference != stripped_ref && Self::is_valid_python_identifier(stripped_ref)))
767 {
768 continue;
769 }
770
771 if !references.contains(&reference_lower) && !reported_refs.contains_key(&reference_lower) {
772 let full_match = cap.get(0).unwrap();
773 let col = full_match.start();
774
775 let code_spans = ctx.code_spans();
777 if Self::is_in_code_span(line_num + 1, col, &code_spans) {
778 continue;
779 }
780
781 let line_start_byte = ctx.line_offsets[line_num];
783 let byte_pos = line_start_byte + col;
784
785 if ctx.is_in_jinja_range(byte_pos) {
787 continue;
788 }
789
790 if crate::utils::code_block_utils::CodeBlockUtils::is_in_code_block(
792 &ctx.code_blocks,
793 byte_pos,
794 ) {
795 continue;
796 }
797
798 if ctx.is_in_html_comment(byte_pos) {
800 continue;
801 }
802
803 if Self::is_in_html_tag(ctx, byte_pos) {
805 continue;
806 }
807
808 if is_in_math_context(ctx, byte_pos) {
810 continue;
811 }
812
813 if is_in_table_cell(ctx, line_num + 1, col) {
815 continue;
816 }
817
818 let byte_end = byte_pos + (full_match.end() - full_match.start());
819
820 let mut is_covered = false;
822 for &(range_start, range_end) in &covered_ranges {
823 if range_start <= byte_pos && byte_end <= range_end {
824 is_covered = true;
826 break;
827 }
828 if range_start > byte_end {
829 break;
831 }
832 }
833
834 if is_covered {
835 continue;
836 }
837
838 let line_chars: Vec<char> = line.chars().collect();
843 if col > 0 && col <= line_chars.len() && line_chars.get(col - 1) == Some(&']') {
844 let mut bracket_count = 1; let mut check_pos = col.saturating_sub(2);
847 let mut found_opening = false;
848
849 while check_pos > 0 && check_pos < line_chars.len() {
850 match line_chars.get(check_pos) {
851 Some(&']') => bracket_count += 1,
852 Some(&'[') => {
853 bracket_count -= 1;
854 if bracket_count == 0 {
855 if check_pos == 0 || line_chars.get(check_pos - 1) != Some(&'\\') {
857 found_opening = true;
858 }
859 break;
860 }
861 }
862 _ => {}
863 }
864 if check_pos == 0 {
865 break;
866 }
867 check_pos = check_pos.saturating_sub(1);
868 }
869
870 if found_opening {
871 continue;
873 }
874 }
875
876 let before_text = &line[..col];
879 if before_text.contains("\\]") {
880 if let Some(escaped_close_pos) = before_text.rfind("\\]") {
882 let search_text = &before_text[..escaped_close_pos];
883 if search_text.contains("\\[") {
884 continue;
886 }
887 }
888 }
889
890 let match_len = full_match.end() - full_match.start();
891 undefined.push((line_num, col, match_len, reference.to_string()));
892 reported_refs.insert(reference_lower, true);
893 }
894 }
895 }
896 }
897 }
898
899 undefined
900 }
901}
902
903impl Rule for MD052ReferenceLinkImages {
904 fn name(&self) -> &'static str {
905 "MD052"
906 }
907
908 fn description(&self) -> &'static str {
909 "Reference links and images should use a reference that exists"
910 }
911
912 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
913 let content = ctx.content;
914 let mut warnings = Vec::new();
915
916 if !content.contains('[') {
918 return Ok(warnings);
919 }
920
921 let mkdocs_mode = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
923
924 let references = self.extract_references(content, mkdocs_mode);
925
926 let lines = ctx.raw_lines();
928 for (line_num, col, match_len, reference) in self.find_undefined_references(&references, ctx, mkdocs_mode) {
929 let line_content = lines.get(line_num).unwrap_or(&"");
930
931 let (start_line, start_col, end_line, end_col) =
933 calculate_match_range(line_num + 1, line_content, col, match_len);
934
935 warnings.push(LintWarning {
936 rule_name: Some(self.name().to_string()),
937 line: start_line,
938 column: start_col,
939 end_line,
940 end_column: end_col,
941 message: format!("Reference '{reference}' not found"),
942 severity: Severity::Warning,
943 fix: None,
944 });
945 }
946
947 Ok(warnings)
948 }
949
950 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
952 ctx.content.is_empty() || !ctx.likely_has_links_or_images()
954 }
955
956 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
957 let content = ctx.content;
958 Ok(content.to_string())
960 }
961
962 fn as_any(&self) -> &dyn std::any::Any {
963 self
964 }
965
966 fn default_config_section(&self) -> Option<(String, toml::Value)> {
967 let json_value = serde_json::to_value(&self.config).ok()?;
968 Some((
969 self.name().to_string(),
970 crate::rule_config_serde::json_to_toml_value(&json_value)?,
971 ))
972 }
973
974 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
975 where
976 Self: Sized,
977 {
978 let rule_config = crate::rule_config_serde::load_rule_config::<MD052Config>(config);
979 Box::new(Self::from_config_struct(rule_config))
980 }
981}
982
983#[cfg(test)]
984mod tests {
985 use super::*;
986 use crate::lint_context::LintContext;
987
988 #[test]
989 fn test_valid_reference_link() {
990 let rule = MD052ReferenceLinkImages::new();
991 let content = "[text][ref]\n\n[ref]: https://example.com";
992 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
993 let result = rule.check(&ctx).unwrap();
994
995 assert_eq!(result.len(), 0);
996 }
997
998 #[test]
999 fn test_undefined_reference_link() {
1000 let rule = MD052ReferenceLinkImages::new();
1001 let content = "[text][undefined]";
1002 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1003 let result = rule.check(&ctx).unwrap();
1004
1005 assert_eq!(result.len(), 1);
1006 assert!(result[0].message.contains("Reference 'undefined' not found"));
1007 }
1008
1009 #[test]
1010 fn test_valid_reference_image() {
1011 let rule = MD052ReferenceLinkImages::new();
1012 let content = "![alt][img]\n\n[img]: image.jpg";
1013 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1014 let result = rule.check(&ctx).unwrap();
1015
1016 assert_eq!(result.len(), 0);
1017 }
1018
1019 #[test]
1020 fn test_undefined_reference_image() {
1021 let rule = MD052ReferenceLinkImages::new();
1022 let content = "![alt][missing]";
1023 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1024 let result = rule.check(&ctx).unwrap();
1025
1026 assert_eq!(result.len(), 1);
1027 assert!(result[0].message.contains("Reference 'missing' not found"));
1028 }
1029
1030 #[test]
1031 fn test_case_insensitive_references() {
1032 let rule = MD052ReferenceLinkImages::new();
1033 let content = "[Text][REF]\n\n[ref]: https://example.com";
1034 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1035 let result = rule.check(&ctx).unwrap();
1036
1037 assert_eq!(result.len(), 0);
1038 }
1039
1040 #[test]
1041 fn test_shortcut_reference_valid() {
1042 let rule = MD052ReferenceLinkImages::new();
1043 let content = "[ref]\n\n[ref]: https://example.com";
1044 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1045 let result = rule.check(&ctx).unwrap();
1046
1047 assert_eq!(result.len(), 0);
1048 }
1049
1050 #[test]
1051 fn test_shortcut_reference_undefined_with_shortcut_syntax_enabled() {
1052 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1055 shortcut_syntax: true,
1056 ..Default::default()
1057 });
1058 let content = "[undefined]";
1059 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1060 let result = rule.check(&ctx).unwrap();
1061
1062 assert_eq!(result.len(), 1);
1063 assert!(result[0].message.contains("Reference 'undefined' not found"));
1064 }
1065
1066 #[test]
1067 fn test_shortcut_reference_not_checked_by_default() {
1068 let rule = MD052ReferenceLinkImages::new();
1070 let content = "[undefined]";
1071 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1072 let result = rule.check(&ctx).unwrap();
1073
1074 assert_eq!(result.len(), 0);
1076 }
1077
1078 #[test]
1079 fn test_inline_links_ignored() {
1080 let rule = MD052ReferenceLinkImages::new();
1081 let content = "[text](https://example.com)";
1082 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1083 let result = rule.check(&ctx).unwrap();
1084
1085 assert_eq!(result.len(), 0);
1086 }
1087
1088 #[test]
1089 fn test_inline_images_ignored() {
1090 let rule = MD052ReferenceLinkImages::new();
1091 let content = "";
1092 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1093 let result = rule.check(&ctx).unwrap();
1094
1095 assert_eq!(result.len(), 0);
1096 }
1097
1098 #[test]
1099 fn test_references_in_code_blocks_ignored() {
1100 let rule = MD052ReferenceLinkImages::new();
1101 let content = "```\n[undefined]\n```\n\n[ref]: https://example.com";
1102 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1103 let result = rule.check(&ctx).unwrap();
1104
1105 assert_eq!(result.len(), 0);
1106 }
1107
1108 #[test]
1109 fn test_references_in_inline_code_ignored() {
1110 let rule = MD052ReferenceLinkImages::new();
1111 let content = "`[undefined]`";
1112 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1113 let result = rule.check(&ctx).unwrap();
1114
1115 assert_eq!(result.len(), 0);
1117 }
1118
1119 #[test]
1120 fn test_comprehensive_inline_code_detection() {
1121 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1123 shortcut_syntax: true,
1124 ..Default::default()
1125 });
1126 let content = r#"# Test
1127
1128This `[inside]` should be ignored.
1129This [outside] should be flagged.
1130Reference links `[text][ref]` in code are ignored.
1131Regular reference [text][missing] should be flagged.
1132Images `![alt][img]` in code are ignored.
1133Regular image ![alt][badimg] should be flagged.
1134
1135Multiple `[one]` and `[two]` in code ignored, but [three] is not.
1136
1137```
1138[code block content] should be ignored
1139```
1140
1141`Multiple [refs] in [same] code span` ignored."#;
1142
1143 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1144 let result = rule.check(&ctx).unwrap();
1145
1146 assert_eq!(result.len(), 4);
1148
1149 let messages: Vec<&str> = result.iter().map(|w| &*w.message).collect();
1150 assert!(messages.iter().any(|m| m.contains("outside")));
1151 assert!(messages.iter().any(|m| m.contains("missing")));
1152 assert!(messages.iter().any(|m| m.contains("badimg")));
1153 assert!(messages.iter().any(|m| m.contains("three")));
1154
1155 assert!(!messages.iter().any(|m| m.contains("inside")));
1157 assert!(!messages.iter().any(|m| m.contains("one")));
1158 assert!(!messages.iter().any(|m| m.contains("two")));
1159 assert!(!messages.iter().any(|m| m.contains("refs")));
1160 assert!(!messages.iter().any(|m| m.contains("same")));
1161 }
1162
1163 #[test]
1164 fn test_multiple_undefined_references() {
1165 let rule = MD052ReferenceLinkImages::new();
1166 let content = "[link1][ref1] [link2][ref2] [link3][ref3]";
1167 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1168 let result = rule.check(&ctx).unwrap();
1169
1170 assert_eq!(result.len(), 3);
1171 assert!(result[0].message.contains("ref1"));
1172 assert!(result[1].message.contains("ref2"));
1173 assert!(result[2].message.contains("ref3"));
1174 }
1175
1176 #[test]
1177 fn test_mixed_valid_and_undefined() {
1178 let rule = MD052ReferenceLinkImages::new();
1179 let content = "[valid][ref] [invalid][missing]\n\n[ref]: https://example.com";
1180 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1181 let result = rule.check(&ctx).unwrap();
1182
1183 assert_eq!(result.len(), 1);
1184 assert!(result[0].message.contains("missing"));
1185 }
1186
1187 #[test]
1188 fn test_empty_reference() {
1189 let rule = MD052ReferenceLinkImages::new();
1190 let content = "[text][]\n\n[ref]: https://example.com";
1191 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1192 let result = rule.check(&ctx).unwrap();
1193
1194 assert_eq!(result.len(), 1);
1196 }
1197
1198 #[test]
1199 fn test_escaped_brackets_ignored() {
1200 let rule = MD052ReferenceLinkImages::new();
1201 let content = "\\[not a link\\]";
1202 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1203 let result = rule.check(&ctx).unwrap();
1204
1205 assert_eq!(result.len(), 0);
1206 }
1207
1208 #[test]
1209 fn test_list_items_ignored() {
1210 let rule = MD052ReferenceLinkImages::new();
1211 let content = "- [undefined]\n* [another]\n+ [third]";
1212 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1213 let result = rule.check(&ctx).unwrap();
1214
1215 assert_eq!(result.len(), 0);
1217 }
1218
1219 #[test]
1220 fn test_output_example_section_ignored() {
1221 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1223 shortcut_syntax: true,
1224 ..Default::default()
1225 });
1226 let content = "## Output\n\n[undefined]\n\n## Normal Section\n\n[missing]";
1227 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1228 let result = rule.check(&ctx).unwrap();
1229
1230 assert_eq!(result.len(), 1);
1232 assert!(result[0].message.contains("missing"));
1233 }
1234
1235 #[test]
1236 fn test_reference_definitions_in_code_blocks_ignored() {
1237 let rule = MD052ReferenceLinkImages::new();
1238 let content = "[link][ref]\n\n```\n[ref]: https://example.com\n```";
1239 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1240 let result = rule.check(&ctx).unwrap();
1241
1242 assert_eq!(result.len(), 1);
1244 assert!(result[0].message.contains("ref"));
1245 }
1246
1247 #[test]
1248 fn test_multiple_references_to_same_undefined() {
1249 let rule = MD052ReferenceLinkImages::new();
1250 let content = "[first][missing] [second][missing] [third][missing]";
1251 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1252 let result = rule.check(&ctx).unwrap();
1253
1254 assert_eq!(result.len(), 1);
1256 assert!(result[0].message.contains("missing"));
1257 }
1258
1259 #[test]
1260 fn test_reference_with_special_characters() {
1261 let rule = MD052ReferenceLinkImages::new();
1262 let content = "[text][ref-with-hyphens]\n\n[ref-with-hyphens]: https://example.com";
1263 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1264 let result = rule.check(&ctx).unwrap();
1265
1266 assert_eq!(result.len(), 0);
1267 }
1268
1269 #[test]
1270 fn test_issue_51_html_attribute_not_reference() {
1271 let rule = MD052ReferenceLinkImages::new();
1273 let content = r#"# Example
1274
1275## Test
1276
1277Want to fill out this form?
1278
1279<form method="post">
1280 <input type="email" name="fields[email]" id="drip-email" placeholder="email@domain.com">
1281</form>"#;
1282 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1283 let result = rule.check(&ctx).unwrap();
1284
1285 assert_eq!(
1286 result.len(),
1287 0,
1288 "HTML attributes with square brackets should not be flagged as undefined references"
1289 );
1290 }
1291
1292 #[test]
1293 fn test_extract_references() {
1294 let rule = MD052ReferenceLinkImages::new();
1295 let content = "[ref1]: url1\n[Ref2]: url2\n[REF3]: url3";
1296 let refs = rule.extract_references(content, false);
1297
1298 assert_eq!(refs.len(), 3);
1299 assert!(refs.contains("ref1"));
1300 assert!(refs.contains("ref2"));
1301 assert!(refs.contains("ref3"));
1302 }
1303
1304 #[test]
1305 fn test_inline_code_not_flagged() {
1306 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1308 shortcut_syntax: true,
1309 ..Default::default()
1310 });
1311
1312 let content = r#"# Test
1314
1315Configure with `["JavaScript", "GitHub", "Node.js"]` in your settings.
1316
1317Also, `[todo]` is not a reference link.
1318
1319But this [reference] should be flagged.
1320
1321And this `[inline code]` should not be flagged.
1322"#;
1323
1324 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1325 let warnings = rule.check(&ctx).unwrap();
1326
1327 assert_eq!(warnings.len(), 1, "Should only flag one undefined reference");
1329 assert!(warnings[0].message.contains("'reference'"));
1330 }
1331
1332 #[test]
1333 fn test_code_block_references_ignored() {
1334 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1336 shortcut_syntax: true,
1337 ..Default::default()
1338 });
1339
1340 let content = r#"# Test
1341
1342```markdown
1343[undefined] reference in code block
1344![undefined] image in code block
1345```
1346
1347[real-undefined] reference outside
1348"#;
1349
1350 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1351 let warnings = rule.check(&ctx).unwrap();
1352
1353 assert_eq!(warnings.len(), 1);
1355 assert!(warnings[0].message.contains("'real-undefined'"));
1356 }
1357
1358 #[test]
1359 fn test_html_comments_ignored() {
1360 let rule = MD052ReferenceLinkImages::new();
1362
1363 let content = r#"<!--- write fake_editor.py 'import sys\nopen(*sys.argv[1:], mode="wt").write("2 3 4 4 2 3 2")' -->
1365<!--- set_env EDITOR 'python3 fake_editor.py' -->
1366
1367```bash
1368$ python3 vote.py
13693 votes for: 2
13702 votes for: 3, 4
1371```"#;
1372 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1373 let result = rule.check(&ctx).unwrap();
1374 assert_eq!(result.len(), 0, "Should not flag [1:] inside HTML comments");
1375
1376 let content = r#"<!-- This is [ref1] and [ref2][ref3] -->
1378Normal [text][undefined]
1379<!-- Another [comment][with] references -->"#;
1380 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1381 let result = rule.check(&ctx).unwrap();
1382 assert_eq!(
1383 result.len(),
1384 1,
1385 "Should only flag the undefined reference outside comments"
1386 );
1387 assert!(result[0].message.contains("undefined"));
1388
1389 let content = r#"<!--
1391[ref1]
1392[ref2][ref3]
1393-->
1394[actual][undefined]"#;
1395 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1396 let result = rule.check(&ctx).unwrap();
1397 assert_eq!(
1398 result.len(),
1399 1,
1400 "Should not flag references in multi-line HTML comments"
1401 );
1402 assert!(result[0].message.contains("undefined"));
1403
1404 let content = r#"<!-- Comment with [1:] pattern -->
1406Valid [link][ref]
1407<!-- More [refs][in][comments] -->
1408![image][missing]
1409
1410[ref]: https://example.com"#;
1411 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1412 let result = rule.check(&ctx).unwrap();
1413 assert_eq!(result.len(), 1, "Should only flag missing image reference");
1414 assert!(result[0].message.contains("missing"));
1415 }
1416
1417 #[test]
1418 fn test_frontmatter_ignored() {
1419 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1422 shortcut_syntax: true,
1423 ..Default::default()
1424 });
1425
1426 let content = r#"---
1428layout: post
1429title: "My Jekyll Post"
1430date: 2023-01-01
1431categories: blog
1432tags: ["test", "example"]
1433author: John Doe
1434---
1435
1436# My Blog Post
1437
1438This is the actual markdown content that should be linted.
1439
1440[undefined] reference should be flagged.
1441
1442## Section 1
1443
1444Some content here."#;
1445 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1446 let result = rule.check(&ctx).unwrap();
1447
1448 assert_eq!(
1450 result.len(),
1451 1,
1452 "Should only flag the undefined reference outside frontmatter"
1453 );
1454 assert!(result[0].message.contains("undefined"));
1455
1456 let content = r#"+++
1458title = "My Post"
1459tags = ["example", "test"]
1460+++
1461
1462# Content
1463
1464[missing] reference should be flagged."#;
1465 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1466 let result = rule.check(&ctx).unwrap();
1467 assert_eq!(
1468 result.len(),
1469 1,
1470 "Should only flag the undefined reference outside TOML frontmatter"
1471 );
1472 assert!(result[0].message.contains("missing"));
1473 }
1474
1475 #[test]
1476 fn test_mkdocs_snippet_markers_not_flagged() {
1477 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1480 shortcut_syntax: true,
1481 ..Default::default()
1482 });
1483
1484 let content = r#"# Document with MkDocs Snippets
1486
1487Some content here.
1488
1489# -8<- [start:remote-content]
1490
1491This is the remote content section.
1492
1493# -8<- [end:remote-content]
1494
1495More content here.
1496
1497<!-- --8<-- [start:another-section] -->
1498Content in another section
1499<!-- --8<-- [end:another-section] -->"#;
1500 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1501 let result = rule.check(&ctx).unwrap();
1502
1503 assert_eq!(
1505 result.len(),
1506 0,
1507 "Should not flag MkDocs snippet markers as undefined references"
1508 );
1509
1510 let content = r#"# Document
1513
1514# -8<- [start:section]
1515Content with [reference] inside snippet section
1516# -8<- [end:section]
1517
1518Regular [undefined] reference outside snippet markers."#;
1519 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1520 let result = rule.check(&ctx).unwrap();
1521
1522 assert_eq!(
1523 result.len(),
1524 2,
1525 "Should flag undefined references but skip snippet marker lines"
1526 );
1527 assert!(result[0].message.contains("reference"));
1529 assert!(result[1].message.contains("undefined"));
1530
1531 let content = r#"# Document
1533
1534# -8<- [start:section]
1535# -8<- [end:section]"#;
1536 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1537 let result = rule.check(&ctx).unwrap();
1538
1539 assert_eq!(
1540 result.len(),
1541 2,
1542 "In standard mode, snippet markers should be flagged as undefined references"
1543 );
1544 }
1545
1546 #[test]
1547 fn test_pandoc_citations_not_flagged() {
1548 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1551 shortcut_syntax: true,
1552 ..Default::default()
1553 });
1554
1555 let content = r#"# Research Paper
1556
1557We are using the **bookdown** package [@R-bookdown] in this sample book.
1558This was built on top of R Markdown and **knitr** [@xie2015].
1559
1560Multiple citations [@citation1; @citation2; @citation3] are also supported.
1561
1562Regular [undefined] reference should still be flagged.
1563"#;
1564 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1565 let result = rule.check(&ctx).unwrap();
1566
1567 assert_eq!(
1569 result.len(),
1570 1,
1571 "Should only flag the undefined reference, not Pandoc citations"
1572 );
1573 assert!(result[0].message.contains("undefined"));
1574 }
1575
1576 #[test]
1577 fn test_pandoc_inline_footnotes_not_flagged() {
1578 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1581 shortcut_syntax: true,
1582 ..Default::default()
1583 });
1584
1585 let content = r#"# Math Document
1586
1587You can use math in footnotes like this^[where we mention $p = \frac{a}{b}$].
1588
1589Another footnote^[with some text and a [link](https://example.com)].
1590
1591But this [reference] without ^ should be flagged.
1592"#;
1593 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1594 let result = rule.check(&ctx).unwrap();
1595
1596 assert_eq!(
1598 result.len(),
1599 1,
1600 "Should only flag the regular reference, not inline footnotes"
1601 );
1602 assert!(result[0].message.contains("reference"));
1603 }
1604
1605 #[test]
1606 fn test_github_alerts_not_flagged() {
1607 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1610 shortcut_syntax: true,
1611 ..Default::default()
1612 });
1613
1614 let content = r#"# Document with GitHub Alerts
1616
1617> [!NOTE]
1618> This is a note alert.
1619
1620> [!TIP]
1621> This is a tip alert.
1622
1623> [!IMPORTANT]
1624> This is an important alert.
1625
1626> [!WARNING]
1627> This is a warning alert.
1628
1629> [!CAUTION]
1630> This is a caution alert.
1631
1632Regular content with [undefined] reference."#;
1633 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1634 let result = rule.check(&ctx).unwrap();
1635
1636 assert_eq!(
1638 result.len(),
1639 1,
1640 "Should only flag the undefined reference, not GitHub alerts"
1641 );
1642 assert!(result[0].message.contains("undefined"));
1643 assert_eq!(result[0].line, 18); let content = r#"> [!TIP]
1647> Here's a useful tip about [something].
1648> Multiple lines are allowed.
1649
1650[something] is mentioned but not defined."#;
1651 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1652 let result = rule.check(&ctx).unwrap();
1653
1654 assert_eq!(result.len(), 1, "Should flag undefined reference");
1658 assert!(result[0].message.contains("something"));
1659
1660 let content = r#"> [!NOTE]
1662> See [reference] for more details.
1663
1664[reference]: https://example.com"#;
1665 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1666 let result = rule.check(&ctx).unwrap();
1667
1668 assert_eq!(result.len(), 0, "Should not flag GitHub alerts or defined references");
1670 }
1671
1672 #[test]
1673 fn test_ignore_config() {
1674 let config = MD052Config {
1676 shortcut_syntax: true,
1677 ignore: vec!["Vec".to_string(), "HashMap".to_string(), "Option".to_string()],
1678 };
1679 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1680
1681 let content = r#"# Document with Custom Types
1682
1683Use [Vec] for dynamic arrays.
1684Use [HashMap] for key-value storage.
1685Use [Option] for nullable values.
1686Use [Result] for error handling.
1687"#;
1688 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1689 let result = rule.check(&ctx).unwrap();
1690
1691 assert_eq!(result.len(), 1, "Should only flag names not in ignore");
1693 assert!(result[0].message.contains("Result"));
1694 }
1695
1696 #[test]
1697 fn test_ignore_case_insensitive() {
1698 let config = MD052Config {
1700 shortcut_syntax: true,
1701 ignore: vec!["Vec".to_string()],
1702 };
1703 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1704
1705 let content = r#"# Case Insensitivity Test
1706
1707[Vec] should be ignored.
1708[vec] should also be ignored (different case, same match).
1709[VEC] should also be ignored (different case, same match).
1710[undefined] should be flagged (not in ignore list).
1711"#;
1712 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1713 let result = rule.check(&ctx).unwrap();
1714
1715 assert_eq!(result.len(), 1, "Should only flag non-ignored reference");
1717 assert!(result[0].message.contains("undefined"));
1718 }
1719
1720 #[test]
1721 fn test_ignore_empty_by_default() {
1722 let rule = MD052ReferenceLinkImages::new();
1724
1725 let content = "[text][undefined]";
1726 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1727 let result = rule.check(&ctx).unwrap();
1728
1729 assert_eq!(result.len(), 1);
1731 assert!(result[0].message.contains("undefined"));
1732 }
1733
1734 #[test]
1735 fn test_ignore_with_reference_links() {
1736 let config = MD052Config {
1738 shortcut_syntax: false,
1739 ignore: vec!["CustomType".to_string()],
1740 };
1741 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1742
1743 let content = r#"# Test
1744
1745See [documentation][CustomType] for details.
1746See [other docs][MissingRef] for more.
1747"#;
1748 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1749 let result = rule.check(&ctx).unwrap();
1750
1751 for (i, w) in result.iter().enumerate() {
1753 eprintln!("Warning {}: {}", i, w.message);
1754 }
1755
1756 assert_eq!(result.len(), 1, "Expected 1 warning, got {}", result.len());
1759 assert!(
1760 result[0].message.contains("missingref"),
1761 "Expected 'missingref' in message: {}",
1762 result[0].message
1763 );
1764 }
1765
1766 #[test]
1767 fn test_ignore_multiple() {
1768 let config = MD052Config {
1770 shortcut_syntax: true,
1771 ignore: vec![
1772 "i32".to_string(),
1773 "u64".to_string(),
1774 "String".to_string(),
1775 "Arc".to_string(),
1776 "Mutex".to_string(),
1777 ],
1778 };
1779 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1780
1781 let content = r#"# Types
1782
1783[i32] [u64] [String] [Arc] [Mutex] [Box]
1784"#;
1785 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1786 let result = rule.check(&ctx).unwrap();
1787
1788 assert_eq!(result.len(), 1);
1792 assert!(result[0].message.contains("Box"));
1793 }
1794}