Skip to main content

react_auditor/rules/performance/
no_ambiguous_labels.rs

1use std::collections::HashMap;
2
3use oxc_ast::ast::Program;
4use oxc_ast_visit::{Visit, walk};
5use oxc_semantic::Semantic;
6
7use crate::rules::{Rule, RuleFinding, RuleMeta, Severity};
8
9pub struct NoAmbiguousLabels;
10
11const RULE_META: RuleMeta = RuleMeta {
12    id: "no-ambiguous-labels",
13    default_severity: Severity::Warning,
14    category: "accessibility",
15    description: "No duplicate or ambiguous label text",
16};
17
18impl Rule for NoAmbiguousLabels {
19    fn meta(&self) -> &RuleMeta {
20        &RULE_META
21    }
22
23    fn run(&self, program: &Program, _semantic: &Semantic, source_text: &str) -> Vec<RuleFinding> {
24        let mut collector = AmbiguousLabelCollector {
25            findings: Vec::new(),
26            source: source_text,
27            label_texts: HashMap::new(),
28        };
29        collector.visit_program(program);
30        collector.findings
31    }
32}
33
34struct AmbiguousLabelCollector<'a> {
35    findings: Vec<RuleFinding>,
36    source: &'a str,
37    label_texts: HashMap<String, Vec<usize>>,
38}
39
40impl<'a> AmbiguousLabelCollector<'a> {
41    fn add_finding(&mut self, start: usize, label: &str) {
42        let line = self.source[..start].lines().count().max(1);
43        let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
44        self.findings.push(RuleFinding {
45            line,
46            column: col + 1,
47            message: format!(
48                "Duplicate label text \"{label}\" — labels must be unique for accessibility"
49            ),
50        });
51    }
52}
53
54impl<'a> Visit<'a> for AmbiguousLabelCollector<'a> {
55    fn visit_jsx_element(&mut self, el: &oxc_ast::ast::JSXElement<'a>) {
56        let is_label = matches!(
57            &el.opening_element.name,
58            oxc_ast::ast::JSXElementName::Identifier(ident)
59                if ident.name.as_str() == "label"
60        );
61
62        if is_label {
63            let mut text = String::new();
64            for child in &el.children {
65                if let oxc_ast::ast::JSXChild::Text(t) = child {
66                    let trimmed = t.value.trim();
67                    if !trimmed.is_empty() {
68                        text.push_str(trimmed);
69                        text.push(' ');
70                    }
71                }
72            }
73            let label = text.trim().to_lowercase();
74            if !label.is_empty() {
75                if let Some(positions) = self.label_texts.get(&label) {
76                    if positions.len() == 1 {
77                        self.add_finding(positions[0], &label);
78                    }
79                    self.add_finding(el.opening_element.span.start as usize, &label);
80                }
81                self.label_texts
82                    .entry(label)
83                    .or_default()
84                    .push(el.opening_element.span.start as usize);
85            }
86        }
87
88        walk::walk_jsx_element(self, el);
89    }
90}