use std::collections::HashMap;
use std::path::PathBuf;
use crate::linter::diagnostic::{Diagnostic, Severity};
use super::{Rule, RuleContext};
pub struct DuplicateLabel;
impl Rule for DuplicateLabel {
fn id(&self) -> &'static str {
"duplicate-label"
}
fn default_severity(&self) -> Severity {
Severity::Warning
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let mut seen: HashMap<&str, usize> = HashMap::new();
for label in ctx.model.labels() {
let count = seen.entry(label.name.as_str()).or_insert(0);
*count += 1;
let message = if *count > 1 {
Some(format!("label `{}` is defined more than once", label.name))
} else {
ctx.resolution
.and_then(|resolution| cross_file_message(ctx, resolution, &label.name))
};
if let Some(message) = message {
sink.push(Diagnostic {
rule: self.id(),
severity: self.default_severity(),
path: PathBuf::new(),
start: usize::from(label.range.start()),
end: usize::from(label.range.end()),
message,
fix: None,
});
}
}
}
}
fn cross_file_message(
ctx: &RuleContext<'_>,
resolution: &crate::project::ResolvedLabels,
name: &str,
) -> Option<String> {
let others: Vec<String> = resolution
.definers(ctx.path, name)
.iter()
.filter(|definer| definer.as_path() != ctx.path)
.map(|definer| format!("`{}`", definer.display()))
.collect();
if others.is_empty() {
return None;
}
Some(format!(
"label `{name}` is also defined in {}",
others.join(", ")
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
use crate::semantic::SemanticModel;
use crate::syntax::SyntaxNode;
fn findings(src: &str) -> Vec<Diagnostic> {
let root = SyntaxNode::new_root(parse(src).green);
let model = SemanticModel::build(&root);
let ctx = RuleContext {
path: std::path::Path::new("x.tex"),
root: &root,
model: &model,
resolution: None,
citations: None,
};
let mut out = Vec::new();
DuplicateLabel.check_file(&ctx, &mut out);
out
}
#[test]
fn flags_only_the_second_definition() {
let src = "\\label{a}\\label{a}\n";
let out = findings(src);
assert_eq!(out.len(), 1);
assert_eq!(out[0].rule, "duplicate-label");
assert_eq!((out[0].start, out[0].end), (9, 18));
}
#[test]
fn caret_excludes_an_over_attached_second_group() {
let out = findings("\\label{a}\\label{a}\n{x}\n");
assert_eq!(out.len(), 1);
assert_eq!((out[0].start, out[0].end), (9, 18));
}
#[test]
fn distinct_keys_are_fine() {
assert!(findings("\\label{a}\\label{b}\n").is_empty());
}
#[test]
fn three_definitions_flag_two() {
assert_eq!(findings("\\label{a}\\label{a}\\label{a}\n").len(), 2);
}
use crate::project::ResolvedLabels;
use crate::project::graph::{FileFacts, IncludeGraph};
use smol_str::SmolStr;
fn two_file_resolution(main_labels: &[&str], other_labels: &[&str]) -> ResolvedLabels {
let graph = IncludeGraph::build(
&[
FileFacts {
path: PathBuf::from("main.tex"),
include_edges: vec![crate::project::IncludeEdgeKey {
kind: crate::project::IncludeKind::Input,
target: crate::project::IncludeTarget::Path(PathBuf::from("other.tex")),
}],
},
FileFacts {
path: PathBuf::from("other.tex"),
include_edges: Vec::new(),
},
],
None,
);
let names = |list: &[&str]| list.iter().map(SmolStr::new).collect::<Vec<_>>();
ResolvedLabels::build(
&[
(PathBuf::from("main.tex"), names(main_labels), true),
(PathBuf::from("other.tex"), names(other_labels), false),
],
&graph,
)
}
fn cross_findings(src: &str, path: &str, resolution: &ResolvedLabels) -> Vec<Diagnostic> {
let root = SyntaxNode::new_root(parse(src).green);
let model = SemanticModel::build(&root);
let ctx = RuleContext {
path: std::path::Path::new(path),
root: &root,
model: &model,
resolution: Some(resolution),
citations: None,
};
let mut out = Vec::new();
DuplicateLabel.check_file(&ctx, &mut out);
out
}
#[test]
fn cross_file_duplicate_names_the_other_file() {
let r = two_file_resolution(&["shared"], &["shared"]);
let out = cross_findings("\\label{shared}\n", "main.tex", &r);
assert_eq!(out.len(), 1);
assert_eq!(out[0].rule, "duplicate-label");
assert!(
out[0].message.contains("also defined in `other.tex`"),
"got: {}",
out[0].message
);
}
#[test]
fn cross_file_unique_key_is_fine() {
let r = two_file_resolution(&["only-main"], &["only-other"]);
assert!(cross_findings("\\label{only-main}\n", "main.tex", &r).is_empty());
}
#[test]
fn intra_and_cross_file_do_not_double_report_first_occurrence() {
let r = two_file_resolution(&["dup"], &["dup"]);
let out = cross_findings("\\label{dup}\\label{dup}\n", "main.tex", &r);
assert_eq!(out.len(), 2);
assert!(out[0].message.contains("also defined in"));
assert!(out[1].message.contains("defined more than once"));
}
}