rumdl_lib/rules/
md024_no_duplicate_heading.rs

1use toml;
2
3use crate::rule::{LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
4use crate::rule_config_serde::RuleConfig;
5use crate::utils::range_utils::calculate_match_range;
6use std::collections::{HashMap, HashSet};
7
8mod md024_config;
9use md024_config::MD024Config;
10
11#[derive(Clone, Debug, Default)]
12pub struct MD024NoDuplicateHeading {
13    config: MD024Config,
14}
15
16impl MD024NoDuplicateHeading {
17    pub fn new(allow_different_nesting: bool, siblings_only: bool) -> Self {
18        Self {
19            config: MD024Config {
20                allow_different_nesting,
21                siblings_only,
22            },
23        }
24    }
25
26    pub fn from_config_struct(config: MD024Config) -> Self {
27        Self { config }
28    }
29}
30
31impl Rule for MD024NoDuplicateHeading {
32    fn name(&self) -> &'static str {
33        "MD024"
34    }
35
36    fn description(&self) -> &'static str {
37        "Multiple headings with the same content"
38    }
39
40    fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
41        // Early return for empty content
42        if ctx.lines.is_empty() {
43            return Ok(Vec::new());
44        }
45
46        let mut warnings = Vec::new();
47        let mut seen_headings: HashSet<String> = HashSet::new();
48        let mut seen_headings_per_level: HashMap<u8, HashSet<String>> = HashMap::new();
49
50        // For siblings_only mode, track heading hierarchy
51        let mut current_section_path: Vec<(u8, String)> = Vec::new(); // Stack of (level, heading_text)
52        let mut seen_siblings: HashMap<String, HashSet<String>> = HashMap::new(); // parent_path -> set of child headings
53
54        // Process headings using cached heading information
55        for (line_num, line_info) in ctx.lines.iter().enumerate() {
56            if let Some(heading) = &line_info.heading {
57                // Skip empty headings
58                if heading.text.is_empty() {
59                    continue;
60                }
61
62                let heading_key = heading.text.clone();
63                let level = heading.level;
64
65                // Calculate precise character range for the heading text content
66                let text_start_in_line = if let Some(pos) = line_info.content.find(&heading.text) {
67                    pos
68                } else {
69                    // Fallback: find after hash markers
70                    let trimmed = line_info.content.trim_start();
71                    let hash_count = trimmed.chars().take_while(|&c| c == '#').count();
72                    let after_hashes = &trimmed[hash_count..];
73                    let text_start_in_trimmed = after_hashes.find(&heading.text).unwrap_or(0);
74                    (line_info.content.len() - trimmed.len()) + hash_count + text_start_in_trimmed
75                };
76
77                let (start_line, start_col, end_line, end_col) =
78                    calculate_match_range(line_num + 1, &line_info.content, text_start_in_line, heading.text.len());
79
80                if self.config.siblings_only {
81                    // Update the section path based on the current heading level
82                    while !current_section_path.is_empty() && current_section_path.last().unwrap().0 >= level {
83                        current_section_path.pop();
84                    }
85
86                    // Build parent path for sibling detection
87                    let parent_path = current_section_path
88                        .iter()
89                        .map(|(_, text)| text.as_str())
90                        .collect::<Vec<_>>()
91                        .join("/");
92
93                    // Check if this heading is a duplicate among its siblings
94                    let siblings = seen_siblings.entry(parent_path.clone()).or_default();
95                    if siblings.contains(&heading_key) {
96                        warnings.push(LintWarning {
97                            rule_name: Some(self.name()),
98                            message: format!("Duplicate heading: '{}'.", heading.text),
99                            line: start_line,
100                            column: start_col,
101                            end_line,
102                            end_column: end_col,
103                            severity: Severity::Warning,
104                            fix: None,
105                        });
106                    } else {
107                        siblings.insert(heading_key.clone());
108                    }
109
110                    // Add current heading to the section path
111                    current_section_path.push((level, heading_key.clone()));
112                } else if self.config.allow_different_nesting {
113                    // Only flag duplicates at the same level
114                    let seen = seen_headings_per_level.entry(level).or_default();
115                    if seen.contains(&heading_key) {
116                        warnings.push(LintWarning {
117                            rule_name: Some(self.name()),
118                            message: format!("Duplicate heading: '{}'.", heading.text),
119                            line: start_line,
120                            column: start_col,
121                            end_line,
122                            end_column: end_col,
123                            severity: Severity::Warning,
124                            fix: None,
125                        });
126                    } else {
127                        seen.insert(heading_key.clone());
128                    }
129                } else {
130                    // Flag all duplicates, regardless of level
131                    if seen_headings.contains(&heading_key) {
132                        warnings.push(LintWarning {
133                            rule_name: Some(self.name()),
134                            message: format!("Duplicate heading: '{}'.", heading.text),
135                            line: start_line,
136                            column: start_col,
137                            end_line,
138                            end_column: end_col,
139                            severity: Severity::Warning,
140                            fix: None,
141                        });
142                    } else {
143                        seen_headings.insert(heading_key.clone());
144                    }
145                }
146            }
147        }
148
149        Ok(warnings)
150    }
151
152    fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
153        // MD024 does not support auto-fixing. Removing duplicate headings is not a safe or meaningful fix.
154        Ok(ctx.content.to_string())
155    }
156
157    /// Get the category of this rule for selective processing
158    fn category(&self) -> RuleCategory {
159        RuleCategory::Heading
160    }
161
162    /// Check if this rule should be skipped
163    fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
164        ctx.lines.iter().all(|line| line.heading.is_none())
165    }
166
167    fn as_any(&self) -> &dyn std::any::Any {
168        self
169    }
170
171    fn as_maybe_document_structure(&self) -> Option<&dyn crate::rule::MaybeDocumentStructure> {
172        None
173    }
174
175    fn default_config_section(&self) -> Option<(String, toml::Value)> {
176        let default_config = MD024Config::default();
177        let json_value = serde_json::to_value(&default_config).ok()?;
178        let toml_value = crate::rule_config_serde::json_to_toml_value(&json_value)?;
179
180        if let toml::Value::Table(table) = toml_value {
181            if !table.is_empty() {
182                Some((MD024Config::RULE_NAME.to_string(), toml::Value::Table(table)))
183            } else {
184                None
185            }
186        } else {
187            None
188        }
189    }
190
191    fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
192    where
193        Self: Sized,
194    {
195        let rule_config = crate::rule_config_serde::load_rule_config::<MD024Config>(config);
196        Box::new(Self::from_config_struct(rule_config))
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::lint_context::LintContext;
204
205    fn run_test(content: &str, config: MD024Config) -> LintResult {
206        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
207        let rule = MD024NoDuplicateHeading::from_config_struct(config);
208        rule.check(&ctx)
209    }
210
211    fn run_fix_test(content: &str, config: MD024Config) -> Result<String, LintError> {
212        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
213        let rule = MD024NoDuplicateHeading::from_config_struct(config);
214        rule.fix(&ctx)
215    }
216
217    #[test]
218    fn test_no_duplicate_headings() {
219        let content = r#"# First Heading
220
221Some content here.
222
223## Second Heading
224
225More content.
226
227### Third Heading
228
229Even more content.
230
231## Fourth Heading
232
233Final content."#;
234
235        let config = MD024Config::default();
236        let result = run_test(content, config);
237        assert!(result.is_ok());
238        let warnings = result.unwrap();
239        assert_eq!(warnings.len(), 0);
240    }
241
242    #[test]
243    fn test_duplicate_headings_same_level() {
244        let content = r#"# First Heading
245
246Some content here.
247
248## Second Heading
249
250More content.
251
252## Second Heading
253
254This is a duplicate."#;
255
256        let config = MD024Config::default();
257        let result = run_test(content, config);
258        assert!(result.is_ok());
259        let warnings = result.unwrap();
260        assert_eq!(warnings.len(), 1);
261        assert_eq!(warnings[0].message, "Duplicate heading: 'Second Heading'.");
262        assert_eq!(warnings[0].line, 9);
263    }
264
265    #[test]
266    fn test_duplicate_headings_different_levels_default() {
267        let content = r#"# Main Title
268
269Some content.
270
271## Main Title
272
273This has the same text but different level."#;
274
275        let config = MD024Config::default();
276        let result = run_test(content, config);
277        assert!(result.is_ok());
278        let warnings = result.unwrap();
279        assert_eq!(warnings.len(), 1);
280        assert_eq!(warnings[0].message, "Duplicate heading: 'Main Title'.");
281        assert_eq!(warnings[0].line, 5);
282    }
283
284    #[test]
285    fn test_duplicate_headings_different_levels_allow_different_nesting() {
286        let content = r#"# Main Title
287
288Some content.
289
290## Main Title
291
292This has the same text but different level."#;
293
294        let config = MD024Config {
295            allow_different_nesting: true,
296            siblings_only: false,
297        };
298        let result = run_test(content, config);
299        assert!(result.is_ok());
300        let warnings = result.unwrap();
301        assert_eq!(warnings.len(), 0);
302    }
303
304    #[test]
305    fn test_case_sensitivity() {
306        let content = r#"# First Heading
307
308Some content.
309
310## first heading
311
312Different case.
313
314### FIRST HEADING
315
316All caps."#;
317
318        let config = MD024Config::default();
319        let result = run_test(content, config);
320        assert!(result.is_ok());
321        let warnings = result.unwrap();
322        // The rule is case-sensitive, so these should not be duplicates
323        assert_eq!(warnings.len(), 0);
324    }
325
326    #[test]
327    fn test_headings_with_trailing_punctuation() {
328        let content = r#"# First Heading!
329
330Some content.
331
332## First Heading!
333
334Same with punctuation.
335
336### First Heading
337
338Without punctuation."#;
339
340        let config = MD024Config::default();
341        let result = run_test(content, config);
342        assert!(result.is_ok());
343        let warnings = result.unwrap();
344        assert_eq!(warnings.len(), 1);
345        assert_eq!(warnings[0].message, "Duplicate heading: 'First Heading!'.");
346    }
347
348    #[test]
349    fn test_headings_with_inline_formatting() {
350        let content = r#"# **Bold Heading**
351
352Some content.
353
354## *Italic Heading*
355
356More content.
357
358### **Bold Heading**
359
360Duplicate with same formatting.
361
362#### `Code Heading`
363
364Code formatted.
365
366##### `Code Heading`
367
368Duplicate code formatted."#;
369
370        let config = MD024Config::default();
371        let result = run_test(content, config);
372        assert!(result.is_ok());
373        let warnings = result.unwrap();
374        assert_eq!(warnings.len(), 2);
375        assert_eq!(warnings[0].message, "Duplicate heading: '**Bold Heading**'.");
376        assert_eq!(warnings[1].message, "Duplicate heading: '`Code Heading`'.");
377    }
378
379    #[test]
380    fn test_headings_in_different_sections() {
381        let content = r#"# Section One
382
383## Subsection
384
385Some content.
386
387# Section Two
388
389## Subsection
390
391Same subsection name in different section."#;
392
393        let config = MD024Config::default();
394        let result = run_test(content, config);
395        assert!(result.is_ok());
396        let warnings = result.unwrap();
397        assert_eq!(warnings.len(), 1);
398        assert_eq!(warnings[0].message, "Duplicate heading: 'Subsection'.");
399        assert_eq!(warnings[0].line, 9);
400    }
401
402    #[test]
403    fn test_multiple_duplicates() {
404        let content = r#"# Title
405
406## Subtitle
407
408### Title
409
410#### Subtitle
411
412## Title
413
414### Subtitle"#;
415
416        let config = MD024Config::default();
417        let result = run_test(content, config);
418        assert!(result.is_ok());
419        let warnings = result.unwrap();
420        assert_eq!(warnings.len(), 4);
421        // First duplicate of "Title"
422        assert_eq!(warnings[0].message, "Duplicate heading: 'Title'.");
423        assert_eq!(warnings[0].line, 5);
424        // First duplicate of "Subtitle"
425        assert_eq!(warnings[1].message, "Duplicate heading: 'Subtitle'.");
426        assert_eq!(warnings[1].line, 7);
427        // Second duplicate of "Title"
428        assert_eq!(warnings[2].message, "Duplicate heading: 'Title'.");
429        assert_eq!(warnings[2].line, 9);
430        // Second duplicate of "Subtitle"
431        assert_eq!(warnings[3].message, "Duplicate heading: 'Subtitle'.");
432        assert_eq!(warnings[3].line, 11);
433    }
434
435    #[test]
436    fn test_empty_headings() {
437        let content = r#"#
438
439Some content.
440
441##
442
443More content.
444
445### Non-empty
446
447####
448
449Another empty."#;
450
451        let config = MD024Config::default();
452        let result = run_test(content, config);
453        assert!(result.is_ok());
454        let warnings = result.unwrap();
455        // Empty headings are skipped
456        assert_eq!(warnings.len(), 0);
457    }
458
459    #[test]
460    fn test_unicode_and_special_characters() {
461        let content = r#"# 你好世界
462
463Some content.
464
465## Émojis 🎉🎊
466
467More content.
468
469### 你好世界
470
471Duplicate Chinese.
472
473#### Émojis 🎉🎊
474
475Duplicate emojis.
476
477##### Special <chars> & symbols!
478
479###### Special <chars> & symbols!
480
481Duplicate special chars."#;
482
483        let config = MD024Config::default();
484        let result = run_test(content, config);
485        assert!(result.is_ok());
486        let warnings = result.unwrap();
487        assert_eq!(warnings.len(), 3);
488        assert_eq!(warnings[0].message, "Duplicate heading: '你好世界'.");
489        assert_eq!(warnings[1].message, "Duplicate heading: 'Émojis 🎉🎊'.");
490        assert_eq!(warnings[2].message, "Duplicate heading: 'Special <chars> & symbols!'.");
491    }
492
493    #[test]
494    fn test_allow_different_nesting_with_same_level_duplicates() {
495        let content = r#"# Section One
496
497## Title
498
499### Subsection
500
501## Title
502
503This is a duplicate at the same level.
504
505# Section Two
506
507## Title
508
509Different section, but still a duplicate when allow_different_nesting is true."#;
510
511        let config = MD024Config {
512            allow_different_nesting: true,
513            siblings_only: false,
514        };
515        let result = run_test(content, config);
516        assert!(result.is_ok());
517        let warnings = result.unwrap();
518        assert_eq!(warnings.len(), 2);
519        assert_eq!(warnings[0].message, "Duplicate heading: 'Title'.");
520        assert_eq!(warnings[0].line, 7);
521        assert_eq!(warnings[1].message, "Duplicate heading: 'Title'.");
522        assert_eq!(warnings[1].line, 13);
523    }
524
525    #[test]
526    fn test_atx_style_headings_with_closing_hashes() {
527        let content = r#"# Heading One #
528
529Some content.
530
531## Heading Two ##
532
533More content.
534
535### Heading One ###
536
537Duplicate with different style."#;
538
539        let config = MD024Config::default();
540        let result = run_test(content, config);
541        assert!(result.is_ok());
542        let warnings = result.unwrap();
543        // The heading text excludes the closing hashes, so "Heading One" is a duplicate
544        assert_eq!(warnings.len(), 1);
545        assert_eq!(warnings[0].message, "Duplicate heading: 'Heading One'.");
546        assert_eq!(warnings[0].line, 9);
547    }
548
549    #[test]
550    fn test_fix_method_returns_unchanged() {
551        let content = r#"# Duplicate
552
553## Duplicate
554
555This has duplicates."#;
556
557        let config = MD024Config::default();
558        let result = run_fix_test(content, config);
559        assert!(result.is_ok());
560        assert_eq!(result.unwrap(), content);
561    }
562
563    #[test]
564    fn test_empty_content() {
565        let content = "";
566        let config = MD024Config::default();
567        let result = run_test(content, config);
568        assert!(result.is_ok());
569        let warnings = result.unwrap();
570        assert_eq!(warnings.len(), 0);
571    }
572
573    #[test]
574    fn test_no_headings() {
575        let content = r#"This is just regular text.
576
577No headings anywhere.
578
579Just paragraphs."#;
580
581        let config = MD024Config::default();
582        let result = run_test(content, config);
583        assert!(result.is_ok());
584        let warnings = result.unwrap();
585        assert_eq!(warnings.len(), 0);
586    }
587
588    #[test]
589    fn test_whitespace_differences() {
590        let content = r#"# Heading with spaces
591
592Some content.
593
594##  Heading with spaces
595
596Different amount of spaces.
597
598### Heading with spaces
599
600Exact match."#;
601
602        let config = MD024Config::default();
603        let result = run_test(content, config);
604        assert!(result.is_ok());
605        let warnings = result.unwrap();
606        // The heading text is trimmed, so all three are duplicates
607        assert_eq!(warnings.len(), 2);
608        assert_eq!(warnings[0].message, "Duplicate heading: 'Heading with spaces'.");
609        assert_eq!(warnings[0].line, 5);
610        assert_eq!(warnings[1].message, "Duplicate heading: 'Heading with spaces'.");
611        assert_eq!(warnings[1].line, 9);
612    }
613
614    #[test]
615    fn test_column_positions() {
616        let content = r#"# First
617
618## Second
619
620### First"#;
621
622        let config = MD024Config::default();
623        let result = run_test(content, config);
624        assert!(result.is_ok());
625        let warnings = result.unwrap();
626        assert_eq!(warnings.len(), 1);
627        assert_eq!(warnings[0].line, 5);
628        assert_eq!(warnings[0].column, 5); // After "### "
629        assert_eq!(warnings[0].end_line, 5);
630        assert_eq!(warnings[0].end_column, 10); // End of "First"
631    }
632
633    #[test]
634    fn test_complex_nesting_scenario() {
635        let content = r#"# Main Document
636
637## Introduction
638
639### Overview
640
641## Implementation
642
643### Overview
644
645This Overview is in a different section.
646
647## Conclusion
648
649### Overview
650
651Another Overview in yet another section."#;
652
653        let config = MD024Config {
654            allow_different_nesting: true,
655            siblings_only: false,
656        };
657        let result = run_test(content, config);
658        assert!(result.is_ok());
659        let warnings = result.unwrap();
660        // When allow_different_nesting is true, only same-level duplicates are flagged
661        assert_eq!(warnings.len(), 2);
662        assert_eq!(warnings[0].message, "Duplicate heading: 'Overview'.");
663        assert_eq!(warnings[0].line, 9);
664        assert_eq!(warnings[1].message, "Duplicate heading: 'Overview'.");
665        assert_eq!(warnings[1].line, 15);
666    }
667
668    #[test]
669    fn test_setext_style_headings() {
670        let content = r#"Main Title
671==========
672
673Some content.
674
675Second Title
676------------
677
678More content.
679
680Main Title
681==========
682
683Duplicate setext."#;
684
685        let config = MD024Config::default();
686        let result = run_test(content, config);
687        assert!(result.is_ok());
688        let warnings = result.unwrap();
689        assert_eq!(warnings.len(), 1);
690        assert_eq!(warnings[0].message, "Duplicate heading: 'Main Title'.");
691        assert_eq!(warnings[0].line, 11);
692    }
693
694    #[test]
695    fn test_mixed_heading_styles() {
696        let content = r#"# ATX Title
697
698Some content.
699
700ATX Title
701=========
702
703Same text, different style."#;
704
705        let config = MD024Config::default();
706        let result = run_test(content, config);
707        assert!(result.is_ok());
708        let warnings = result.unwrap();
709        assert_eq!(warnings.len(), 1);
710        assert_eq!(warnings[0].message, "Duplicate heading: 'ATX Title'.");
711        assert_eq!(warnings[0].line, 5);
712    }
713
714    #[test]
715    fn test_heading_with_links() {
716        let content = r#"# [Link Text](http://example.com)
717
718Some content.
719
720## [Link Text](http://example.com)
721
722Duplicate heading with link.
723
724### [Different Link](http://example.com)
725
726Not a duplicate."#;
727
728        let config = MD024Config::default();
729        let result = run_test(content, config);
730        assert!(result.is_ok());
731        let warnings = result.unwrap();
732        assert_eq!(warnings.len(), 1);
733        assert_eq!(
734            warnings[0].message,
735            "Duplicate heading: '[Link Text](http://example.com)'."
736        );
737        assert_eq!(warnings[0].line, 5);
738    }
739
740    #[test]
741    fn test_consecutive_duplicates() {
742        let content = r#"# Title
743
744## Title
745
746### Title
747
748Three in a row."#;
749
750        let config = MD024Config::default();
751        let result = run_test(content, config);
752        assert!(result.is_ok());
753        let warnings = result.unwrap();
754        assert_eq!(warnings.len(), 2);
755        assert_eq!(warnings[0].message, "Duplicate heading: 'Title'.");
756        assert_eq!(warnings[0].line, 3);
757        assert_eq!(warnings[1].message, "Duplicate heading: 'Title'.");
758        assert_eq!(warnings[1].line, 5);
759    }
760
761    #[test]
762    fn test_siblings_only_config() {
763        let content = r#"# Section One
764
765## Subsection
766
767### Details
768
769# Section Two
770
771## Subsection
772
773Different parent sections, so not siblings - no warning expected."#;
774
775        let config = MD024Config {
776            allow_different_nesting: false,
777            siblings_only: true,
778        };
779        let result = run_test(content, config);
780        assert!(result.is_ok());
781        let warnings = result.unwrap();
782        // With siblings_only, these are not flagged because they're under different parents
783        assert_eq!(warnings.len(), 0);
784    }
785
786    #[test]
787    fn test_siblings_only_with_actual_siblings() {
788        let content = r#"# Main Section
789
790## First Subsection
791
792### Details
793
794## Second Subsection
795
796### Details
797
798The two 'Details' headings are siblings under different subsections - no warning.
799
800## First Subsection
801
802This 'First Subsection' IS a sibling duplicate."#;
803
804        let config = MD024Config {
805            allow_different_nesting: false,
806            siblings_only: true,
807        };
808        let result = run_test(content, config);
809        assert!(result.is_ok());
810        let warnings = result.unwrap();
811        // Only the duplicate "First Subsection" at the same level should be flagged
812        assert_eq!(warnings.len(), 1);
813        assert_eq!(warnings[0].message, "Duplicate heading: 'First Subsection'.");
814        assert_eq!(warnings[0].line, 13);
815    }
816
817    #[test]
818    fn test_code_spans_in_headings() {
819        let content = r#"# `code` in heading
820
821Some content.
822
823## `code` in heading
824
825Duplicate with code span."#;
826
827        let config = MD024Config::default();
828        let result = run_test(content, config);
829        assert!(result.is_ok());
830        let warnings = result.unwrap();
831        assert_eq!(warnings.len(), 1);
832        assert_eq!(warnings[0].message, "Duplicate heading: '`code` in heading'.");
833        assert_eq!(warnings[0].line, 5);
834    }
835
836    #[test]
837    fn test_very_long_heading() {
838        let long_text = "This is a very long heading that goes on and on and on and contains many words to test how the rule handles long headings";
839        let content = format!("# {long_text}\n\nSome content.\n\n## {long_text}\n\nDuplicate long heading.");
840
841        let config = MD024Config::default();
842        let result = run_test(&content, config);
843        assert!(result.is_ok());
844        let warnings = result.unwrap();
845        assert_eq!(warnings.len(), 1);
846        assert_eq!(warnings[0].message, format!("Duplicate heading: '{long_text}'."));
847        assert_eq!(warnings[0].line, 5);
848    }
849
850    #[test]
851    fn test_heading_with_html_entities() {
852        let content = r#"# Title &amp; More
853
854Some content.
855
856## Title &amp; More
857
858Duplicate with HTML entity."#;
859
860        let config = MD024Config::default();
861        let result = run_test(content, config);
862        assert!(result.is_ok());
863        let warnings = result.unwrap();
864        assert_eq!(warnings.len(), 1);
865        assert_eq!(warnings[0].message, "Duplicate heading: 'Title &amp; More'.");
866        assert_eq!(warnings[0].line, 5);
867    }
868
869    #[test]
870    fn test_three_duplicates_different_nesting() {
871        let content = r#"# Main
872
873## Main
874
875### Main
876
877#### Main
878
879All same text, different levels."#;
880
881        let config = MD024Config {
882            allow_different_nesting: true,
883            siblings_only: false,
884        };
885        let result = run_test(content, config);
886        assert!(result.is_ok());
887        let warnings = result.unwrap();
888        // With allow_different_nesting, there should be no warnings
889        assert_eq!(warnings.len(), 0);
890    }
891}