use std::path::Path;
use crate::parser::{LexConfig, parse_with_flavor};
use crate::project::{ResolvedCitations, ResolvedLabels};
use crate::semantic::SemanticModel;
use crate::syntax::SyntaxNode;
use super::diagnostic::Diagnostic;
use super::rules::{RuleContext, RuleRegistry, StreamVisitor, fixable_registry, registry};
use super::suppression::SuppressionMap;
pub fn check_document(path: &Path, text: &str, config: impl Into<LexConfig>) -> Vec<Diagnostic> {
let parsed = parse_with_flavor(text, config);
let mut diagnostics: Vec<Diagnostic> = parsed
.errors
.iter()
.map(|err| Diagnostic::from_parse(path.to_path_buf(), err))
.collect();
let root = SyntaxNode::new_root(parsed.green);
let model = SemanticModel::build(&root);
diagnostics.extend(lint_document(path, &root, &model, None, None));
diagnostics
}
pub fn check_document_fixable(
path: &Path,
text: &str,
config: impl Into<LexConfig>,
) -> Vec<Diagnostic> {
let parsed = parse_with_flavor(text, config);
let root = SyntaxNode::new_root(parsed.green);
let model = SemanticModel::build(&root);
lint_with(fixable_registry(), path, &root, &model, None, None)
}
pub fn lint_document(
path: &Path,
root: &SyntaxNode,
model: &SemanticModel,
resolution: Option<&ResolvedLabels>,
citations: Option<&ResolvedCitations>,
) -> Vec<Diagnostic> {
lint_with(registry(), path, root, model, resolution, citations)
}
fn lint_with(
reg: &RuleRegistry,
path: &Path,
root: &SyntaxNode,
model: &SemanticModel,
resolution: Option<&ResolvedLabels>,
citations: Option<&ResolvedCitations>,
) -> Vec<Diagnostic> {
let ctx = RuleContext::new(path, root, model, resolution, citations);
let mut diagnostics: Vec<Diagnostic> = Vec::new();
let mut visitors: Vec<Box<dyn StreamVisitor>> =
reg.rules.iter().filter_map(|r| r.stream()).collect();
if reg.any_node_rules || !visitors.is_empty() {
for el in root.descendants_with_tokens() {
for &i in ®.by_kind[el.kind() as usize] {
reg.rules[i].check(&el, &ctx, &mut diagnostics);
}
for v in &mut visitors {
v.visit(&el, &ctx, &mut diagnostics);
}
}
}
for v in &mut visitors {
v.finish(&ctx, &mut diagnostics);
}
for rule in ®.rules {
rule.check_file(&ctx, &mut diagnostics);
}
let suppress = SuppressionMap::build(root);
diagnostics.retain(|d| !suppress.is_suppressed(d.rule, d.start, d.end));
for d in &mut diagnostics {
d.path = path.to_path_buf();
}
diagnostics.sort_by_key(|d| (d.start, d.end, d.rule));
diagnostics
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
fn lint(src: &str) -> Vec<Diagnostic> {
let root = SyntaxNode::new_root(parse(src).green);
let model = SemanticModel::build(&root);
lint_document(Path::new("x.tex"), &root, &model, None, None)
}
fn rules_of(src: &str) -> Vec<&'static str> {
lint(src).iter().map(|d| d.rule).collect()
}
#[test]
fn collects_both_rule_families() {
let rules = rules_of("\\label{a}\\label{a}\n{\\bf x}\n");
assert_eq!(rules, vec!["duplicate-label", "deprecated-command"]);
}
#[test]
fn node_directive_suppresses_following_command() {
let out = lint("% badness-ignore deprecated-command: legacy\n{\\bf x}\n");
assert!(out.is_empty(), "expected suppression, got: {out:?}");
}
#[test]
fn node_directive_only_targets_named_rule() {
let out = lint("% badness-ignore duplicate-label: nope\n{\\bf x}\n");
assert_eq!(rules_of_diags(&out), vec!["deprecated-command"]);
}
#[test]
fn node_directive_does_not_leak_past_first_target() {
let out = lint("% badness-ignore deprecated-command: legacy\n{\\bf x}\n\n{\\it y}\n");
assert_eq!(rules_of_diags(&out), vec!["deprecated-command"]);
}
#[test]
fn file_directive_suppresses_all_occurrences() {
let out = lint("% badness-ignore-file deprecated-command: legacy\n{\\bf x}\n{\\it y}\n");
assert!(
out.is_empty(),
"expected file-wide suppression, got: {out:?}"
);
}
fn rules_of_diags(diags: &[Diagnostic]) -> Vec<&'static str> {
diags.iter().map(|d| d.rule).collect()
}
}