use std::path::PathBuf;
use crate::linter::diagnostic::{Diagnostic, Severity};
use super::{BibRule, BibRuleContext, Example};
const EXAMPLES: &[Example] = &[Example {
caption: "A typo'd macro name (`cpu` for `cup`):",
source: "@string{cup = {Cambridge University Press}}\n\
@book{turing50, title = {Draft}, publisher = cpu}\n",
}];
pub struct UndefinedString;
impl BibRule for UndefinedString {
fn id(&self) -> &'static str {
"undefined-string"
}
fn default_severity(&self) -> Severity {
Severity::Warning
}
fn description(&self) -> &'static str {
"Flag an `@string` macro used in a field value but defined nowhere in \
the file (the twelve month macros `jan`..`dec` are predefined). \
Usually a typo'd macro name or a missing `@string` definition; BibTeX \
errors on it at build time. In a multi-file bibliography the \
definition may live in another `.bib`, so a use resolved there is a \
false positive -- cross-file `@string` resolution is not modeled yet. \
Report-only: the fix (define the macro or correct the name) is a \
meaning-level edit left to the author."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn check_file(&self, ctx: &BibRuleContext<'_>, sink: &mut Vec<Diagnostic>) {
for use_ in ctx.model.undefined_string_uses() {
sink.push(Diagnostic {
rule: self.id(),
severity: self.default_severity(),
path: PathBuf::new(),
start: usize::from(use_.range.start()),
end: usize::from(use_.range.end()),
message: format!("`@string` macro `{}` is used but never defined", use_.name),
fix: None,
related: Vec::new(),
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bib::parse;
use crate::bib::semantic::Model;
fn findings(src: &str) -> Vec<Diagnostic> {
let root = parse(src).syntax();
let model = Model::build(&root);
let ctx = BibRuleContext {
path: std::path::Path::new("x.bib"),
root: &root,
model: &model,
db: crate::bib::semantic::builtin(),
};
let mut out = Vec::new();
UndefinedString.check_file(&ctx, &mut out);
out
}
#[test]
fn flags_undefined_use() {
let out = findings("@book{k, publisher = nope}\n");
assert_eq!(out.len(), 1);
assert_eq!(out[0].rule, "undefined-string");
assert!(out[0].message.contains("nope"));
}
#[test]
fn in_file_def_is_fine() {
assert!(findings("@string{cup = {C}}\n@book{k, publisher = cup}\n").is_empty());
}
#[test]
fn month_macro_is_fine() {
assert!(findings("@article{k, month = jan}\n").is_empty());
}
#[test]
fn number_value_is_not_a_use() {
assert!(findings("@article{k, year = 2020}\n").is_empty());
}
#[test]
fn underlines_the_use() {
let src = "@book{k, publisher = nope}\n";
let out = findings(src);
assert_eq!(out.len(), 1);
assert_eq!(&src[out[0].start..out[0].end], "nope");
}
}