use std::collections::HashSet;
use std::path::PathBuf;
use crate::linter::diagnostic::{Diagnostic, Severity};
use crate::project::{PackageKind, PackageTarget, collect_package_edges};
use super::{Example, Rule, RuleContext};
const EXAMPLES: &[Example] = &[Example {
caption: "The same package loaded twice:",
source: "\\usepackage{amsmath}\n\\usepackage{amsmath}\n",
}];
pub struct DuplicatePackage;
impl Rule for DuplicatePackage {
fn id(&self) -> &'static str {
"duplicate-package"
}
fn default_severity(&self) -> Severity {
Severity::Warning
}
fn description(&self) -> &'static str {
"Flag a package loaded more than once in the same file with \
`\\usepackage`/`\\RequirePackage` (which share one package namespace). \
LaTeX loads a given package only once; a second load is redundant and, \
when the options disagree, an option-clash error. No autofix: removing a \
load can drop options the survivor lacks, and which load to keep is the \
author's call. Class loads (`\\documentclass`/`\\LoadClass`) are a \
separate concern and are not flagged."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let mut seen: HashSet<PathBuf> = HashSet::new();
for edge in collect_package_edges(ctx.root, None) {
if !matches!(
edge.kind,
PackageKind::UsePackage | PackageKind::RequirePackage
) {
continue;
}
let PackageTarget::Path(path) = &edge.target else {
continue;
};
if seen.insert(path.clone()) {
continue;
}
let name = path
.file_stem()
.and_then(|s| s.to_str())
.map(str::to_owned)
.unwrap_or_else(|| path.display().to_string());
sink.push(Diagnostic {
rule: self.id(),
severity: self.default_severity(),
path: PathBuf::new(),
start: usize::from(edge.range.start()),
end: usize::from(edge.range.end()),
message: format!("package `{name}` is loaded more than once"),
fix: None,
});
}
}
}
#[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::new(std::path::Path::new("x.tex"), &root, &model, None, None);
let mut out = Vec::new();
DuplicatePackage.check_file(&ctx, &mut out);
out
}
#[test]
fn flags_only_the_second_load() {
let out = findings("\\usepackage{amsmath}\n\\usepackage{amsmath}\n");
assert_eq!(out.len(), 1);
assert_eq!(out[0].rule, "duplicate-package");
assert!(
out[0].message.contains("amsmath"),
"got: {}",
out[0].message
);
assert_eq!((out[0].start, out[0].end), (21, 41));
}
#[test]
fn distinct_packages_are_fine() {
assert!(findings("\\usepackage{amsmath}\n\\usepackage{amssymb}\n").is_empty());
}
#[test]
fn comma_list_then_reload_flags_once() {
let out = findings("\\usepackage{amsmath,amssymb}\n\\usepackage{amsmath}\n");
assert_eq!(out.len(), 1);
assert!(
out[0].message.contains("amsmath"),
"got: {}",
out[0].message
);
}
#[test]
fn require_package_and_usepackage_share_namespace() {
let out = findings("\\RequirePackage{tools}\n\\usepackage{tools}\n");
assert_eq!(out.len(), 1);
assert!(out[0].message.contains("tools"), "got: {}", out[0].message);
}
#[test]
fn duplicate_inside_one_list_flags_once() {
let out = findings("\\usepackage{amsmath,amsmath}\n");
assert_eq!(out.len(), 1);
}
#[test]
fn triple_load_flags_twice() {
let out = findings("\\usepackage{a}\n\\usepackage{a}\n\\usepackage{a}\n");
assert_eq!(out.len(), 2);
}
#[test]
fn dynamic_target_is_ignored() {
assert!(findings("\\usepackage{\\pkg}\n\\usepackage{\\pkg}\n").is_empty());
}
#[test]
fn duplicate_documentclass_is_not_this_rules_concern() {
assert!(findings("\\documentclass{article}\n\\documentclass{article}\n").is_empty());
}
}