1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, Severity};
5use crate::utils::range_utils::calculate_match_range;
6use crate::utils::skip_context::{is_in_front_matter, is_in_html_comment, is_in_math_context};
7use lazy_static::lazy_static;
8use regex::Regex;
9
10lazy_static! {
11 static ref REVERSED_LINK_REGEX: Regex =
12 Regex::new(r"\[([^\]]+)\]\(([^)]+)\)|(\([^)]+\))\[([^\]]+)\]").unwrap();
13 static ref REVERSED_LINK_CHECK_REGEX: Regex = Regex::new(
16 r"\(([^)]*(?:\([^)]*\)[^)]*)*)\)\[([^\]]+)\]"
17 ).unwrap();
18
19 static ref ESCAPED_CHARS: Regex = Regex::new(r"\\[\[\]()]").unwrap();
21
22 static ref MATH_CONTEXT: Regex = Regex::new(
24 r"(?:f|g|h|sin|cos|tan|log|ln|exp|matrix|vector|det|lim|sum|prod|int)\s*\([^)]+\)|[∈∉⊆⊂⊃⊇∩∪∧∨¬∀∃∅∞∫∑∏√±×÷≠≤≥≈≡]|\b(?:where|such that|for all|exists|if and only if)\b"
25 ).unwrap();
26
27 static ref FUNCTION_ARRAY_NOTATION: Regex = Regex::new(
29 r"(?:[a-zA-Z_]\w*\([^)]*\)\[[^\]]+\])|(?:\([^)]+\)\[(?:element|index|derivative|integral|component|row|column|entry|coefficient|term)\])"
30 ).unwrap();
31
32 static ref MALFORMED_LINK_PATTERNS: Vec<(Regex, &'static str)> = vec![
34 (Regex::new(r"\(([^)]+)\)\[([^\]]*$)").unwrap(), "missing closing bracket"),
36 (Regex::new(r"\[([^\]]+)\]\(([^)]*$)").unwrap(), "missing closing parenthesis"),
37
38 (Regex::new(r"\{([^}]+)\}\[([^\]]+)\]").unwrap(), "wrong bracket type (curly instead of parentheses)"),
40 (Regex::new(r"\[([^\]]+)\]\{([^}]+)\}").unwrap(), "wrong bracket type (curly instead of parentheses)"),
41
42 (Regex::new(r"\[(https?://[^\]]+)\]\(([^)]+)\)").unwrap(), "URL and text appear to be swapped"),
44 (Regex::new(r"\[(www\.[^\]]+)\]\(([^)]+)\)").unwrap(), "URL and text appear to be swapped"),
45 (Regex::new(r"\[([^\]]*\.[a-z]{2,4}[^\]]*)\]\(([^)]+)\)").unwrap(), "URL and text appear to be swapped"),
46 ];
47}
48
49#[derive(Clone)]
50pub struct MD011NoReversedLinks;
51
52impl MD011NoReversedLinks {
53 fn is_escaped(content: &str, pos: usize) -> bool {
55 if pos == 0 {
56 return false;
57 }
58
59 let mut backslash_count = 0;
60 let mut check_pos = pos - 1;
61
62 loop {
63 if content.chars().nth(check_pos) == Some('\\') {
64 backslash_count += 1;
65 if check_pos == 0 {
66 break;
67 }
68 check_pos -= 1;
69 } else {
70 break;
71 }
72 }
73
74 backslash_count % 2 == 1
75 }
76
77 fn find_reversed_links(content: &str) -> Vec<(usize, usize, String, String)> {
78 let mut results = Vec::new();
79 let mut line_start = 0;
80 let mut current_line = 1;
81
82 for line in content.lines() {
83 if !line.contains('(') || !line.contains('[') || !line.contains(']') || !line.contains(')') {
85 line_start += line.len() + 1;
86 current_line += 1;
87 continue;
88 }
89
90 if MATH_CONTEXT.is_match(line) {
92 line_start += line.len() + 1;
93 current_line += 1;
94 continue;
95 }
96
97 if FUNCTION_ARRAY_NOTATION.is_match(line) {
99 line_start += line.len() + 1;
100 current_line += 1;
101 continue;
102 }
103
104 for cap in REVERSED_LINK_CHECK_REGEX.captures_iter(line) {
105 let url = &cap[1];
107 let text = &cap[2];
108
109 if text.len() < 20
112 && (text.contains("inclusive")
113 || text.contains("exclusive")
114 || text.contains("intersection")
115 || text.contains("union")
116 || text.contains("element")
117 || text.contains("derivative")
118 || text.contains("component")
119 || text.contains("index"))
120 {
121 continue;
122 }
123
124 if url.contains(',') && !url.contains("://") && !url.contains('.') {
126 continue;
127 }
128
129 let start = line_start + cap.get(0).unwrap().start();
130 results.push((current_line, start - line_start + 1, text.to_string(), url.to_string()));
131 }
132 line_start += line.len() + 1; current_line += 1;
134 }
135
136 results
137 }
138
139 fn detect_malformed_link_attempts(&self, line: &str) -> Vec<(usize, usize, String, String)> {
141 let mut results = Vec::new();
142 let mut processed_ranges = Vec::new(); for (pattern, issue_type) in MALFORMED_LINK_PATTERNS.iter() {
145 for cap in pattern.captures_iter(line) {
146 let match_obj = cap.get(0).unwrap();
147 let start = match_obj.start();
148 let len = match_obj.len();
149 let end = start + len;
150
151 if processed_ranges
153 .iter()
154 .any(|(proc_start, proc_end)| (start < *proc_end && end > *proc_start))
155 {
156 continue;
157 }
158
159 if let Some((url, text)) = self.extract_url_and_text_from_match(&cap, issue_type) {
161 if self.looks_like_link_attempt(&url, &text) {
163 results.push((start, len, url, text));
164 processed_ranges.push((start, end));
165 }
166 }
167 }
168 }
169
170 results
171 }
172
173 fn extract_url_and_text_from_match(&self, cap: ®ex::Captures, issue_type: &str) -> Option<(String, String)> {
175 match issue_type {
176 "missing closing bracket" => {
177 Some((cap[1].to_string(), format!("{}]", &cap[2])))
179 }
180 "missing closing parenthesis" => {
181 Some((format!("{})", &cap[2]), cap[1].to_string()))
183 }
184 "wrong bracket type (curly instead of parentheses)" => {
185 if cap.get(0).unwrap().as_str().starts_with('{') {
187 Some((cap[1].to_string(), cap[2].to_string()))
189 } else {
190 Some((cap[2].to_string(), cap[1].to_string()))
192 }
193 }
194 "URL and text appear to be swapped" => {
195 Some((cap[1].to_string(), cap[2].to_string()))
197 }
198 _ => None,
199 }
200 }
201
202 fn looks_like_link_attempt(&self, url: &str, text: &str) -> bool {
204 let url_indicators = [
206 "http://", "https://", "www.", "ftp://", ".com", ".org", ".net", ".edu", ".gov", ".io", ".co",
207 ];
208
209 let has_url_indicator = url_indicators
210 .iter()
211 .any(|indicator| url.to_lowercase().contains(indicator));
212
213 let text_looks_reasonable = text.len() >= 3
215 && text.len() <= 50
216 && !url_indicators
217 .iter()
218 .any(|indicator| text.to_lowercase().contains(indicator))
219 && !text.to_lowercase().starts_with("http")
220 && text.chars().any(|c| c.is_alphabetic()); let url_looks_reasonable =
224 url.len() >= 4 && (has_url_indicator || url.contains('.')) && !url.chars().all(|c| c.is_alphabetic()); has_url_indicator && text_looks_reasonable && url_looks_reasonable
228 }
229}
230
231impl Default for MD011NoReversedLinks {
232 fn default() -> Self {
233 Self
234 }
235}
236
237impl Rule for MD011NoReversedLinks {
238 fn name(&self) -> &'static str {
239 "MD011"
240 }
241
242 fn description(&self) -> &'static str {
243 "Link syntax should not be reversed"
244 }
245
246 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
247 let content = ctx.content;
248 let mut warnings = Vec::new();
249 let mut byte_pos = 0;
250
251 for (line_num, line) in content.lines().enumerate() {
252 for cap in REVERSED_LINK_CHECK_REGEX.captures_iter(line) {
254 let match_obj = cap.get(0).unwrap();
255 let match_start = match_obj.start();
256 let match_end = match_obj.end();
257
258 let match_byte_pos = byte_pos + match_start;
260 if ctx.is_in_code_block_or_span(match_byte_pos) {
261 continue;
262 }
263
264 if is_in_html_comment(content, match_byte_pos) {
266 continue;
267 }
268
269 if is_in_math_context(ctx, match_byte_pos) {
271 continue;
272 }
273
274 if is_in_front_matter(content, line_num) {
276 continue;
277 }
278
279 let match_text = match_obj.as_str();
281
282 if match_start > 0 && Self::is_escaped(line, byte_pos + match_start) {
284 continue;
285 }
286
287 let mut skip_match = false;
289 for esc_match in ESCAPED_CHARS.find_iter(match_text) {
290 let esc_pos = match_start + esc_match.start();
291 if esc_pos > 0 && line.chars().nth(esc_pos.saturating_sub(1)) == Some('\\') {
292 skip_match = true;
293 break;
294 }
295 }
296
297 if skip_match {
298 continue;
299 }
300
301 let remaining = &line[match_end..];
304 if remaining.trim_start().starts_with('(') {
305 continue;
306 }
307
308 let url = &cap[1];
310 let text = &cap[2];
311
312 let (start_line, start_col, end_line, end_col) =
314 calculate_match_range(line_num + 1, line, match_obj.start(), match_obj.len());
315
316 warnings.push(LintWarning {
317 rule_name: Some(self.name()),
318 message: format!("Reversed link syntax: use [{text}]({url}) instead"),
319 line: start_line,
320 column: start_col,
321 end_line,
322 end_column: end_col,
323 severity: Severity::Warning,
324 fix: Some(Fix {
325 range: {
326 let line_start_byte = ctx.line_offsets.get(line_num).copied().unwrap_or(0);
328 let match_start_byte = line_start_byte + match_obj.start();
329 let match_end_byte = match_start_byte + match_obj.len();
330 match_start_byte..match_end_byte
331 },
332 replacement: format!("[{text}]({url})"),
333 }),
334 });
335 }
336
337 let malformed_attempts = self.detect_malformed_link_attempts(line);
339 for (start, len, url, text) in malformed_attempts {
340 let match_byte_pos = byte_pos + start;
342 if ctx.is_in_code_block_or_span(match_byte_pos) {
343 continue;
344 }
345
346 if is_in_html_comment(content, match_byte_pos) {
348 continue;
349 }
350
351 if is_in_math_context(ctx, match_byte_pos) {
353 continue;
354 }
355
356 if is_in_front_matter(content, line_num) {
358 continue;
359 }
360
361 let (start_line, start_col, end_line, end_col) = calculate_match_range(line_num + 1, line, start, len);
363
364 warnings.push(LintWarning {
365 rule_name: Some(self.name()),
366 message: "Malformed link syntax".to_string(),
367 line: start_line,
368 column: start_col,
369 end_line,
370 end_column: end_col,
371 severity: Severity::Warning,
372 fix: Some(Fix {
373 range: {
374 let line_start_byte = ctx.line_offsets.get(line_num).copied().unwrap_or(0);
376 let match_start_byte = line_start_byte + start;
377 let match_end_byte = match_start_byte + len;
378 match_start_byte..match_end_byte
379 },
380 replacement: format!("[{text}]({url})"),
381 }),
382 });
383 }
384
385 byte_pos += line.len() + 1; }
387
388 Ok(warnings)
389 }
390
391 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
392 let content = ctx.content;
393 let mut result = content.to_string();
394 let mut offset: usize = 0;
395
396 for (line_num, column, text, url) in Self::find_reversed_links(content) {
397 let mut pos = 0;
399 for (i, line) in content.lines().enumerate() {
400 if i + 1 == line_num {
401 pos += column - 1;
402 break;
403 }
404 pos += line.len() + 1;
405 }
406
407 if !ctx.is_in_code_block_or_span(pos) {
408 let adjusted_pos = pos + offset;
409 let original_len = format!("({text})[{url}]").len();
410 let replacement = format!("[{text}]({url})");
411 result.replace_range(adjusted_pos..adjusted_pos + original_len, &replacement);
412 if replacement.len() > original_len {
414 offset += replacement.len() - original_len;
415 } else {
416 offset = offset.saturating_sub(original_len - replacement.len());
417 }
418 }
419 }
420
421 Ok(result)
422 }
423
424 fn as_any(&self) -> &dyn std::any::Any {
425 self
426 }
427
428 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
429 ctx.content.is_empty() || !ctx.content.contains('(') || !ctx.content.contains('[')
431 }
432
433 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
434 where
435 Self: Sized,
436 {
437 Box::new(MD011NoReversedLinks)
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use crate::lint_context::LintContext;
445 use crate::utils::skip_context::is_in_front_matter;
446
447 #[test]
448 fn test_capture_group_order_fix() {
449 let rule = MD011NoReversedLinks;
456
457 let content = "Check out (https://example.com)[this link] for more info.";
459 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
460
461 let result = rule.check(&ctx).unwrap();
463 assert_eq!(result.len(), 1);
464 assert!(result[0].message.contains("Reversed link syntax"));
465
466 let fix = result[0].fix.as_ref().unwrap();
468 assert_eq!(fix.replacement, "[this link](https://example.com)");
469 }
470
471 #[test]
472 fn test_multiple_reversed_links() {
473 let rule = MD011NoReversedLinks;
475
476 let content = "Visit (https://example.com)[Example] and (https://test.com)[Test Site].";
477 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
478
479 let result = rule.check(&ctx).unwrap();
480 assert_eq!(result.len(), 2);
481
482 assert_eq!(
484 result[0].fix.as_ref().unwrap().replacement,
485 "[Example](https://example.com)"
486 );
487 assert_eq!(
488 result[1].fix.as_ref().unwrap().replacement,
489 "[Test Site](https://test.com)"
490 );
491 }
492
493 #[test]
494 fn test_normal_links_not_flagged() {
495 let rule = MD011NoReversedLinks;
497
498 let content = "This is a normal [link](https://example.com) and another [link](https://test.com).";
499 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
500
501 let result = rule.check(&ctx).unwrap();
502 assert_eq!(result.len(), 0);
503 }
504
505 #[test]
506 fn debug_capture_groups() {
507 let pattern = r"\(([^)]+)\)\[([^\]]+)\]";
509 let regex = Regex::new(pattern).unwrap();
510
511 let test_text = "(https://example.com)[Click here]";
512
513 if let Some(cap) = regex.captures(test_text) {
514 println!("Full match: {}", &cap[0]);
515 println!("cap[1] (first group): {}", &cap[1]);
516 println!("cap[2] (second group): {}", &cap[2]);
517
518 let current_fix = format!("[{}]({})", &cap[2], &cap[1]);
520 println!("Current fix produces: {current_fix}");
521
522 let rule = MD011NoReversedLinks;
524 let ctx = LintContext::new(test_text, crate::config::MarkdownFlavor::Standard);
525 let result = rule.check(&ctx).unwrap();
526 if !result.is_empty() {
527 println!("Rule fix produces: {}", result[0].fix.as_ref().unwrap().replacement);
528 }
529 }
530 }
531
532 #[test]
533 fn test_front_matter_detection() {
534 let content = r#"---
535title: "My Post"
536tags: ["test", "example"]
537description: "Pattern (like)[this] in frontmatter"
538---
539
540# Content
541
542Regular (https://example.com)[reversed link] that should be flagged.
543
544+++
545title = "TOML frontmatter"
546tags = ["more", "tags"]
547pattern = "(toml)[pattern]"
548+++
549
550# More Content
551
552Another (https://test.com)[reversed] link should be flagged."#;
553
554 for (idx, line) in content.lines().enumerate() {
556 let line_num = idx; let in_fm = is_in_front_matter(content, line_num);
558
559 println!("Line {:2} (0-idx: {:2}): in_fm={:5} | {:?}", idx + 1, idx, in_fm, line);
560
561 if idx <= 4 {
563 assert!(
564 in_fm,
565 "Line {} (0-idx: {}) should be in YAML front matter but got false. Content: {:?}",
566 idx + 1,
567 idx,
568 line
569 );
570 }
571 else if (10..=14).contains(&idx) {
573 assert!(
574 !in_fm,
575 "Line {} (0-idx: {}) should NOT be in front matter (TOML block not at beginning). Content: {:?}",
576 idx + 1,
577 idx,
578 line
579 );
580 }
581 else {
583 assert!(
584 !in_fm,
585 "Line {} (0-idx: {}) should NOT be in front matter but got true. Content: {:?}",
586 idx + 1,
587 idx,
588 line
589 );
590 }
591 }
592 }
593
594 #[test]
595 fn test_malformed_link_detection() {
596 let rule = MD011NoReversedLinks;
597
598 let content = "Check out {https://example.com}[this website].";
600 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
601 let result = rule.check(&ctx).unwrap();
602 assert_eq!(result.len(), 1);
603 assert!(result[0].message.contains("Malformed link syntax"));
604
605 let content = "Visit [https://example.com](Click Here).";
607 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
608 let result = rule.check(&ctx).unwrap();
609 assert_eq!(result.len(), 1);
610 assert!(result[0].message.contains("Malformed link syntax"));
611
612 let content = "This is a [normal link](https://example.com).";
614 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
615 let result = rule.check(&ctx).unwrap();
616 assert_eq!(result.len(), 0);
617
618 let content = "Regular text with [brackets] and (parentheses).";
620 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
621 let result = rule.check(&ctx).unwrap();
622 assert_eq!(result.len(), 0);
623
624 let content = "(example.com)is a test domain.";
626 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
627 let result = rule.check(&ctx).unwrap();
628 assert_eq!(result.len(), 0);
629
630 let content = "(optional)parameter should not be flagged.";
631 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
632 let result = rule.check(&ctx).unwrap();
633 assert_eq!(result.len(), 0);
634 }
635
636 #[test]
637 fn test_malformed_link_fixes() {
638 let rule = MD011NoReversedLinks;
639
640 let content = "Check out {https://example.com}[this website].";
642 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
643 let result = rule.check(&ctx).unwrap();
644 assert_eq!(result.len(), 1);
645 let fix = result[0].fix.as_ref().unwrap();
646 assert_eq!(fix.replacement, "[this website](https://example.com)");
647
648 let content = "Visit [https://example.com](Click Here).";
650 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
651 let result = rule.check(&ctx).unwrap();
652 assert_eq!(result.len(), 1);
653 let fix = result[0].fix.as_ref().unwrap();
654 assert_eq!(fix.replacement, "[Click Here](https://example.com)");
655 }
656
657 #[test]
658 fn test_conservative_detection() {
659 let rule = MD011NoReversedLinks;
660
661 let content = "This (not-a-url)text should be ignored.";
663 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
664 let result = rule.check(&ctx).unwrap();
665 assert_eq!(result.len(), 0);
666
667 let content = "Also [regular text](not a url) should be ignored.";
668 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
669 let result = rule.check(&ctx).unwrap();
670 assert_eq!(result.len(), 0);
671
672 let content = "And {not-url}[not-text] should be ignored.";
673 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
674 let result = rule.check(&ctx).unwrap();
675 assert_eq!(result.len(), 0);
676 }
677
678 #[test]
679 fn test_skip_code_blocks() {
680 let rule = MD011NoReversedLinks;
681
682 let content = r#"Here's an example:
684
685```rust
686// This regex pattern [.!?]+\s*$ should not be flagged
687static ref TRAILING_PUNCTUATION: Regex = Regex::new(r"(?m)[.!?]+\s*$").unwrap();
688```
689
690But this (https://example.com)[reversed link] should be flagged."#;
691
692 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
693 let result = rule.check(&ctx).unwrap();
694
695 assert_eq!(result.len(), 1);
697 assert!(result[0].message.contains("Reversed link syntax"));
698 assert_eq!(result[0].line, 8); }
700
701 #[test]
702 fn test_negative_lookahead() {
703 let rule = MD011NoReversedLinks;
704
705 let content = "This is a reference-style link: (see here)[ref](https://example.com)";
707 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
708 let result = rule.check(&ctx).unwrap();
709 assert_eq!(result.len(), 0, "Should not flag (text)[ref](url) pattern");
710
711 let content = "This is reversed: (https://example.com)[click here]";
713 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
714 let result = rule.check(&ctx).unwrap();
715 assert_eq!(result.len(), 1, "Should flag genuine reversed links");
716
717 let content = "Reference with space: (text)[ref] (url)";
719 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
720 let result = rule.check(&ctx).unwrap();
721 assert_eq!(result.len(), 0, "Should not flag when space before (url)");
722 }
723
724 #[test]
725 fn test_escaped_characters() {
726 let rule = MD011NoReversedLinks;
727
728 let content = r"Escaped: \(not a link\)\[also not\]";
730 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
731 let result = rule.check(&ctx).unwrap();
732 assert_eq!(result.len(), 0, "Should not flag escaped brackets");
733
734 let content = "(https://example.com/path(with)parens)[text]";
736 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
737 let result = rule.check(&ctx).unwrap();
738 assert_eq!(result.len(), 1, "Should still flag URLs with nested parentheses");
739 }
740
741 #[test]
742 fn test_inline_code_patterns() {
743 let rule = MD011NoReversedLinks;
745
746 let content = "I find `inspect.stack()[1].frame` a lot easier to understand (or at least guess about) at a glance than `inspect.stack()[1][0]`.";
748 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
749 let result = rule.check(&ctx).unwrap();
750 assert_eq!(result.len(), 0, "Should not flag ()[1] patterns inside inline code");
751
752 let content = "Use `array()[0]` or `func()[1]` to access elements.";
754 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
755 let result = rule.check(&ctx).unwrap();
756 assert_eq!(result.len(), 0, "Should not flag array access patterns in inline code");
757
758 let content = "Check out (https://example.com)[this link] and use `array()[1]`.";
760 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
761 let result = rule.check(&ctx).unwrap();
762 assert_eq!(result.len(), 1, "Should flag actual reversed link but not code pattern");
763 assert!(result[0].message.contains("Reversed link syntax"));
764
765 let content = r#"
767Here's some code: `func()[1]` and `other()[2]`.
768
769But this is wrong: (https://example.com)[Click here]
770
771```python
772# This should not be flagged
773result = inspect.stack()[1]
774```
775"#;
776 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
777 let result = rule.check(&ctx).unwrap();
778 assert_eq!(result.len(), 1, "Should only flag the actual reversed link");
779 assert_eq!(result[0].line, 4, "Should flag the reversed link on line 4");
780 }
781
782 #[test]
783 fn test_issue_26_specific_case() {
784 let rule = MD011NoReversedLinks;
786
787 let content = r#"The first thing I need to find is the name of the redacted key name, `doc.<key_name_omitted>`. I'll use `SUBSTRING(ATTRIBUTES(doc)[0], 0, 1) == '<c>'` as that test, where `<c>` is different characters. This gets the first attribute from `doc` and uses `SUBSTRING` to get the first character."#;
788 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
789 let result = rule.check(&ctx).unwrap();
790 assert_eq!(
791 result.len(),
792 0,
793 "Should not flag ATTRIBUTES(doc)[0] inside inline code (issue #26)"
794 );
795 }
796}