react_auditor/rules/performance/
no_ambiguous_labels.rs1use 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}