use crate::analysis::ElementIdKind;
use crate::cross_file::diagnostics::{
CrossFileDiagnostic, CrossFileDiagnosticKind, DiagnosticSeverity,
};
use crate::cross_file::registry::{FileId, ModuleRegistry};
use vize_carton::{cstr, CompactString, FxHashMap};
#[derive(Debug, Clone)]
pub struct UniqueIdIssue {
pub id: CompactString,
pub locations: Vec<(FileId, u32)>,
pub in_loop: bool,
pub is_dynamic: bool,
pub kind: ElementIdKind,
}
pub fn analyze_element_ids(
registry: &ModuleRegistry,
) -> (Vec<UniqueIdIssue>, Vec<CrossFileDiagnostic>) {
let mut issues = Vec::new();
let mut diagnostics = Vec::new();
let mut static_ids: FxHashMap<CompactString, Vec<(FileId, u32, bool, ElementIdKind)>> =
FxHashMap::default();
let mut dynamic_ids_in_loops: Vec<(FileId, CompactString, u32, ElementIdKind)> = Vec::new();
for entry in registry.vue_components() {
for id_info in &entry.analysis.element_ids {
if id_info.kind.is_definition() {
if id_info.is_static {
static_ids.entry(id_info.value.clone()).or_default().push((
entry.id,
id_info.start,
id_info.in_loop,
id_info.kind,
));
} else if id_info.in_loop {
dynamic_ids_in_loops.push((
entry.id,
id_info.value.clone(),
id_info.start,
id_info.kind,
));
}
}
}
}
for (id, locations) in &static_ids {
if locations.len() > 1 {
let loc_list: Vec<_> = locations
.iter()
.map(|(file, off, _, _)| (*file, *off))
.collect();
let kind = locations[0].3;
issues.push(UniqueIdIssue {
id: id.clone(),
locations: loc_list.clone(),
in_loop: locations.iter().any(|(_, _, in_loop, _)| *in_loop),
is_dynamic: false,
kind,
});
diagnostics.push(
CrossFileDiagnostic::new(
CrossFileDiagnosticKind::DuplicateElementId {
id: id.clone(),
locations: loc_list,
},
DiagnosticSeverity::Warning,
locations[0].0,
locations[0].1,
cstr!(
"Element ID '{}' is used in {} different locations across files",
id,
locations.len()
),
)
.with_suggestion("Use useId() to generate unique IDs for each component instance"),
);
}
for (file_id, offset, in_loop, kind) in locations {
if *in_loop {
issues.push(UniqueIdIssue {
id: id.clone(),
locations: vec![(*file_id, *offset)],
in_loop: true,
is_dynamic: false,
kind: *kind,
});
diagnostics.push(
CrossFileDiagnostic::new(
CrossFileDiagnosticKind::NonUniqueIdInLoop {
id_expression: id.clone(),
},
DiagnosticSeverity::Error,
*file_id,
*offset,
cstr!("Static ID '{id}' inside v-for will create duplicate IDs",),
)
.with_suggestion("Use a dynamic ID like `:id=\"`item-${index}`\"` or useId()"),
);
}
}
}
for (file_id, id_expr, offset, kind) in dynamic_ids_in_loops {
if !looks_unique(&id_expr) {
issues.push(UniqueIdIssue {
id: id_expr.clone(),
locations: vec![(file_id, offset)],
in_loop: true,
is_dynamic: true,
kind,
});
diagnostics.push(
CrossFileDiagnostic::new(
CrossFileDiagnosticKind::NonUniqueIdInLoop {
id_expression: id_expr.clone(),
},
DiagnosticSeverity::Warning,
file_id,
offset,
cstr!("Dynamic ID '{id_expr}' may not produce unique values",),
)
.with_suggestion("Include a unique identifier like index or item.id"),
);
}
}
(issues, diagnostics)
}
fn looks_unique(expr: &str) -> bool {
let unique_patterns = [
"index",
".id",
".uuid",
".key",
"getid",
"uniqueid",
"nanoid",
"uuid",
"math.random",
"date.now",
"generateid",
];
let expr_lower = expr.to_lowercase();
unique_patterns.iter().any(|p| expr_lower.contains(p))
}
#[cfg(test)]
mod tests {
use super::looks_unique;
#[test]
fn test_looks_unique() {
assert!(looks_unique("`item-${index}`"));
assert!(looks_unique("item.id"));
assert!(looks_unique("generateId()"));
assert!(!looks_unique("'static-id'"));
assert!(!looks_unique("item.name"));
}
}