summaryrefslogtreecommitdiff
path: root/support/texlab/crates/diagnostics
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2024-01-28 03:00:53 +0000
committerNorbert Preining <norbert@preining.info>2024-01-28 03:00:53 +0000
commit7084e3008c8fc947579f46c6b8a08dfd180e72ef (patch)
tree9c99041cba4afefac3859d6a2fe4cea289458468 /support/texlab/crates/diagnostics
parentb93d257f657e619e22b8b7a27446118ce041727e (diff)
CTAN sync 202401280300
Diffstat (limited to 'support/texlab/crates/diagnostics')
-rw-r--r--support/texlab/crates/diagnostics/Cargo.toml10
-rw-r--r--support/texlab/crates/diagnostics/src/build_log.rs104
-rw-r--r--support/texlab/crates/diagnostics/src/chktex.rs121
-rw-r--r--support/texlab/crates/diagnostics/src/citations.rs91
-rw-r--r--support/texlab/crates/diagnostics/src/grammar.rs6
-rw-r--r--support/texlab/crates/diagnostics/src/grammar/bib.rs87
-rw-r--r--support/texlab/crates/diagnostics/src/grammar/tex.rs91
-rw-r--r--support/texlab/crates/diagnostics/src/labels.rs53
-rw-r--r--support/texlab/crates/diagnostics/src/lib.rs85
-rw-r--r--support/texlab/crates/diagnostics/src/manager.rs75
-rw-r--r--support/texlab/crates/diagnostics/src/tests.rs201
-rw-r--r--support/texlab/crates/diagnostics/src/types.rs65
-rw-r--r--support/texlab/crates/diagnostics/src/util.rs28
13 files changed, 578 insertions, 439 deletions
diff --git a/support/texlab/crates/diagnostics/Cargo.toml b/support/texlab/crates/diagnostics/Cargo.toml
index 7de9c6042d..1fb4b2171d 100644
--- a/support/texlab/crates/diagnostics/Cargo.toml
+++ b/support/texlab/crates/diagnostics/Cargo.toml
@@ -7,15 +7,25 @@ edition.workspace = true
rust-version.workspace = true
[dependencies]
+anyhow = "1.0.75"
base-db = { path = "../base-db" }
+dirs = "5.0.1"
+encoding_rs = "0.8.33"
+encoding_rs_io = "0.1.7"
itertools = "0.12.0"
line-index = { path = "../line-index" }
+log = "0.4.20"
+multimap = "0.9.1"
+once_cell = "1.19.0"
+parking_lot = "0.12.1"
+regex = "1.10.2"
rowan = "0.15.15"
rustc-hash = "1.1.0"
syntax = { path = "../syntax" }
url = "2.5.0"
[dev-dependencies]
+expect-test = "1.4.1"
test-utils = { path = "../test-utils" }
[lib]
diff --git a/support/texlab/crates/diagnostics/src/build_log.rs b/support/texlab/crates/diagnostics/src/build_log.rs
index 4c53a35ff8..352e18d0d1 100644
--- a/support/texlab/crates/diagnostics/src/build_log.rs
+++ b/support/texlab/crates/diagnostics/src/build_log.rs
@@ -1,94 +1,54 @@
-use std::borrow::Cow;
-
use base_db::{Document, Workspace};
use line_index::LineCol;
+use multimap::MultiMap;
use rowan::{TextLen, TextRange, TextSize};
use rustc_hash::FxHashMap;
use syntax::BuildError;
use url::Url;
-use crate::{
- types::{Diagnostic, DiagnosticData},
- DiagnosticBuilder, DiagnosticSource,
-};
-
-#[derive(Debug, Default)]
-struct BuildLog {
- errors: FxHashMap<Url, Vec<Diagnostic>>,
-}
+use crate::types::Diagnostic;
-#[derive(Debug, Default)]
-pub struct BuildErrors {
- logs: FxHashMap<Url, BuildLog>,
-}
+pub fn update(
+ workspace: &Workspace,
+ log_document: &Document,
+ results: &mut FxHashMap<Url, MultiMap<Url, Diagnostic>>,
+) -> Option<()> {
+ let mut errors = MultiMap::default();
-impl DiagnosticSource for BuildErrors {
- fn update(&mut self, workspace: &Workspace, log_document: &Document) {
- let mut errors: FxHashMap<Url, Vec<Diagnostic>> = FxHashMap::default();
+ let data = log_document.data.as_log()?;
- let Some(data) = log_document.data.as_log() else {
- return;
- };
+ let parents = workspace.parents(log_document);
+ let root_document = parents.iter().next()?;
- let parents = workspace.parents(log_document);
- let Some(root_document) = parents.iter().next() else {
- return;
- };
+ let base_path = root_document
+ .path
+ .as_deref()
+ .and_then(|path| path.parent())?;
- let Some(base_path) = root_document.path.as_deref().and_then(|path| path.parent()) else {
- return;
+ for error in &data.errors {
+ let full_path = base_path.join(&error.relative_path);
+ let Ok(full_path_uri) = Url::from_file_path(&full_path) else {
+ continue;
};
- for error in &data.errors {
- let full_path = base_path.join(&error.relative_path);
- let Ok(full_path_uri) = Url::from_file_path(&full_path) else {
- continue;
- };
+ let tex_document = workspace.lookup(&full_path_uri).unwrap_or(root_document);
- let tex_document = workspace.lookup(&full_path_uri).unwrap_or(root_document);
+ let range = find_range_of_hint(tex_document, error).unwrap_or_else(|| {
+ let line = error.line.unwrap_or(0);
+ let offset = tex_document
+ .line_index
+ .offset(LineCol { line, col: 0 })
+ .unwrap_or(TextSize::from(0));
- let range = find_range_of_hint(tex_document, error).unwrap_or_else(|| {
- let line = error.line.unwrap_or(0);
- let offset = tex_document
- .line_index
- .offset(LineCol { line, col: 0 })
- .unwrap_or(TextSize::from(0));
+ TextRange::empty(offset)
+ });
- TextRange::empty(offset)
- });
-
- let diagnostic = Diagnostic {
- range,
- data: DiagnosticData::Build(error.clone()),
- };
-
- errors
- .entry(tex_document.uri.clone())
- .or_default()
- .push(diagnostic);
- }
-
- self.logs
- .insert(log_document.uri.clone(), BuildLog { errors });
+ let diagnostic = Diagnostic::Build(range, error.clone());
+ errors.insert(tex_document.uri.clone(), diagnostic);
}
- fn publish<'db>(
- &'db mut self,
- workspace: &'db Workspace,
- builder: &mut DiagnosticBuilder<'db>,
- ) {
- self.logs.retain(|uri, _| workspace.lookup(uri).is_some());
-
- for document in workspace.iter() {
- let Some(log) = self.logs.get(&document.uri) else {
- continue;
- };
-
- for (uri, errors) in &log.errors {
- builder.push_many(uri, errors.iter().map(Cow::Borrowed));
- }
- }
- }
+ results.insert(log_document.uri.clone(), errors);
+ Some(())
}
fn find_range_of_hint(document: &Document, error: &BuildError) -> Option<TextRange> {
diff --git a/support/texlab/crates/diagnostics/src/chktex.rs b/support/texlab/crates/diagnostics/src/chktex.rs
new file mode 100644
index 0000000000..a342f78349
--- /dev/null
+++ b/support/texlab/crates/diagnostics/src/chktex.rs
@@ -0,0 +1,121 @@
+use std::{
+ io::{BufRead, BufReader, Write},
+ path::PathBuf,
+ process::Stdio,
+};
+
+use base_db::{Document, Workspace};
+use encoding_rs_io::DecodeReaderBytesBuilder;
+use line_index::LineCol;
+use once_cell::sync::Lazy;
+use regex::Regex;
+
+use crate::{types::Diagnostic, ChktexError, ChktexSeverity};
+
+#[derive(Debug)]
+pub struct Command {
+ text: String,
+ working_dir: PathBuf,
+ additional_args: Vec<String>,
+}
+
+impl Command {
+ pub fn new(workspace: &Workspace, document: &Document) -> Option<Self> {
+ document.data.as_tex()?;
+
+ let parent = workspace
+ .parents(document)
+ .into_iter()
+ .next()
+ .unwrap_or(document);
+
+ if parent.path.is_none() {
+ log::warn!("Calling ChkTeX on non-local files is not supported yet.");
+ return None;
+ }
+
+ let working_dir = workspace.current_dir(&parent.dir).to_file_path().ok()?;
+ log::debug!("Calling ChkTeX from directory: {}", working_dir.display());
+
+ let text = document.text.clone();
+ let config = &workspace.config().diagnostics.chktex;
+ let additional_args = config.additional_args.clone();
+ Some(Self {
+ text,
+ working_dir,
+ additional_args,
+ })
+ }
+
+ pub fn run(mut self) -> std::io::Result<Vec<Diagnostic>> {
+ let mut args = vec!["-I0".into(), "-f%l:%c:%d:%k:%n:%m\n".into()];
+ args.append(&mut self.additional_args);
+
+ let mut child = std::process::Command::new("chktex")
+ .args(args)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::null())
+ .current_dir(self.working_dir)
+ .spawn()?;
+
+ let stdout = child.stdout.take().unwrap();
+ let reader = std::thread::spawn(move || {
+ let mut diagnostics = Vec::new();
+ let reader = BufReader::new(
+ DecodeReaderBytesBuilder::new()
+ .encoding(Some(encoding_rs::UTF_8))
+ .utf8_passthru(true)
+ .strip_bom(true)
+ .build(stdout),
+ );
+
+ for line in reader.lines().flatten() {
+ let captures = LINE_REGEX.captures(&line).unwrap();
+ let line = captures[1].parse::<u32>().unwrap() - 1;
+ let character = captures[2].parse::<u32>().unwrap() - 1;
+ let digit = captures[3].parse::<u32>().unwrap();
+ let kind = &captures[4];
+ let code = String::from(&captures[5]);
+ let message = captures[6].into();
+ let start = LineCol {
+ line,
+ col: character,
+ };
+
+ let end = LineCol {
+ line,
+ col: character + digit,
+ };
+
+ let severity = match kind {
+ "Message" => ChktexSeverity::Message,
+ "Warning" => ChktexSeverity::Warning,
+ _ => ChktexSeverity::Error,
+ };
+
+ diagnostics.push(Diagnostic::Chktex(ChktexError {
+ start,
+ end,
+ message,
+ severity,
+ code,
+ }));
+ }
+
+ diagnostics
+ });
+
+ let mut stdin = child.stdin.take().unwrap();
+ let bytes = self.text.into_bytes();
+ let writer = std::thread::spawn(move || stdin.write_all(&bytes));
+
+ child.wait()?;
+ writer.join().unwrap()?;
+ let diagnostics = reader.join().unwrap();
+ Ok(diagnostics)
+ }
+}
+
+static LINE_REGEX: Lazy<Regex> =
+ Lazy::new(|| Regex::new("(\\d+):(\\d+):(\\d+):(\\w+):(\\w+):(.*)").unwrap());
diff --git a/support/texlab/crates/diagnostics/src/citations.rs b/support/texlab/crates/diagnostics/src/citations.rs
index 0b750aaadb..403d2f5bfc 100644
--- a/support/texlab/crates/diagnostics/src/citations.rs
+++ b/support/texlab/crates/diagnostics/src/citations.rs
@@ -1,46 +1,22 @@
-use std::borrow::Cow;
-
use base_db::{
semantics::{bib::Entry, tex::Citation},
util::queries::{self, Object},
Document, Project, Workspace,
};
+use multimap::MultiMap;
use rustc_hash::FxHashSet;
+use url::Url;
-use crate::{
- types::{BibError, Diagnostic, DiagnosticData, TexError},
- DiagnosticBuilder, DiagnosticSource,
-};
+use crate::types::{BibError, Diagnostic, TexError};
const MAX_UNUSED_ENTRIES: usize = 1000;
-#[derive(Default)]
-pub struct CitationErrors;
-
-impl DiagnosticSource for CitationErrors {
- fn publish<'db>(
- &'db mut self,
- workspace: &'db Workspace,
- builder: &mut DiagnosticBuilder<'db>,
- ) {
- for document in workspace.iter() {
- let project = workspace.project(document);
- detect_undefined_citations(&project, document, builder);
- detect_unused_entries(&project, document, builder);
- }
-
- detect_duplicate_entries(workspace, builder);
- }
-}
-
-fn detect_undefined_citations<'db>(
- project: &Project<'db>,
- document: &'db Document,
- builder: &mut DiagnosticBuilder<'db>,
-) {
- let Some(data) = document.data.as_tex() else {
- return;
- };
+pub fn detect_undefined_citations<'a>(
+ project: &Project<'a>,
+ document: &'a Document,
+ results: &mut MultiMap<Url, Diagnostic>,
+) -> Option<()> {
+ let data = document.data.as_tex()?;
let entries: FxHashSet<&str> = Entry::find_all(project)
.map(|(_, entry)| entry.name_text())
@@ -49,28 +25,24 @@ fn detect_undefined_citations<'db>(
for citation in &data.semantics.citations {
let name = citation.name_text();
if name != "*" && !entries.contains(name) {
- let diagnostic = Diagnostic {
- range: citation.name.range,
- data: DiagnosticData::Tex(TexError::UndefinedCitation),
- };
-
- builder.push(&document.uri, Cow::Owned(diagnostic));
+ let diagnostic = Diagnostic::Tex(citation.name.range, TexError::UndefinedCitation);
+ results.insert(document.uri.clone(), diagnostic);
}
}
+
+ Some(())
}
-fn detect_unused_entries<'db>(
- project: &Project<'db>,
- document: &'db Document,
- builder: &mut DiagnosticBuilder<'db>,
-) {
- let Some(data) = document.data.as_bib() else {
- return;
- };
+pub fn detect_unused_entries<'a>(
+ project: &Project<'a>,
+ document: &'a Document,
+ results: &mut MultiMap<Url, Diagnostic>,
+) -> Option<()> {
+ let data = document.data.as_bib()?;
// If this is a huge bibliography, then don't bother checking for unused entries.
if data.semantics.entries.len() > MAX_UNUSED_ENTRIES {
- return;
+ return None;
}
let citations: FxHashSet<&str> = Citation::find_all(project)
@@ -79,17 +51,18 @@ fn detect_unused_entries<'db>(
for entry in &data.semantics.entries {
if !citations.contains(entry.name.text.as_str()) {
- let diagnostic = Diagnostic {
- range: entry.name.range,
- data: DiagnosticData::Bib(BibError::UnusedEntry),
- };
-
- builder.push(&document.uri, Cow::Owned(diagnostic));
+ let diagnostic = Diagnostic::Bib(entry.name.range, BibError::UnusedEntry);
+ results.insert(document.uri.clone(), diagnostic);
}
}
+
+ Some(())
}
-fn detect_duplicate_entries<'db>(workspace: &'db Workspace, builder: &mut DiagnosticBuilder<'db>) {
+pub fn detect_duplicate_entries<'a>(
+ workspace: &'a Workspace,
+ results: &mut MultiMap<Url, Diagnostic>,
+) {
for conflict in queries::Conflict::find_all::<Entry>(workspace) {
let others = conflict
.rest
@@ -97,11 +70,7 @@ fn detect_duplicate_entries<'db>(workspace: &'db Workspace, builder: &mut Diagno
.map(|location| (location.document.uri.clone(), location.range))
.collect();
- let diagnostic = Diagnostic {
- range: conflict.main.range,
- data: DiagnosticData::Bib(BibError::DuplicateEntry(others)),
- };
-
- builder.push(&conflict.main.document.uri, Cow::Owned(diagnostic));
+ let diagnostic = Diagnostic::Bib(conflict.main.range, BibError::DuplicateEntry(others));
+ results.insert(conflict.main.document.uri.clone(), diagnostic);
}
}
diff --git a/support/texlab/crates/diagnostics/src/grammar.rs b/support/texlab/crates/diagnostics/src/grammar.rs
index 1a9e3cecda..df05ee6d14 100644
--- a/support/texlab/crates/diagnostics/src/grammar.rs
+++ b/support/texlab/crates/diagnostics/src/grammar.rs
@@ -1,4 +1,2 @@
-mod bib;
-mod tex;
-
-pub use self::{bib::BibSyntaxErrors, tex::TexSyntaxErrors};
+pub mod bib;
+pub mod tex;
diff --git a/support/texlab/crates/diagnostics/src/grammar/bib.rs b/support/texlab/crates/diagnostics/src/grammar/bib.rs
index e101e68f2a..494cdbe4b1 100644
--- a/support/texlab/crates/diagnostics/src/grammar/bib.rs
+++ b/support/texlab/crates/diagnostics/src/grammar/bib.rs
@@ -1,48 +1,35 @@
-use base_db::{Document, DocumentData, Workspace};
+use base_db::{BibDocumentData, Document};
+use multimap::MultiMap;
use rowan::{ast::AstNode, TextRange};
use syntax::bibtex::{self, HasDelims, HasEq, HasName, HasType, HasValue};
+use url::Url;
-use crate::{
- types::{BibError, DiagnosticData},
- util::SimpleDiagnosticSource,
- Diagnostic, DiagnosticBuilder, DiagnosticSource,
-};
+use crate::types::{BibError, Diagnostic};
-#[derive(Default)]
-pub struct BibSyntaxErrors(SimpleDiagnosticSource);
+pub fn update(document: &Document, results: &mut MultiMap<Url, Diagnostic>) -> Option<()> {
+ let data = document.data.as_bib()?;
+ let mut analyzer = Analyzer {
+ data,
+ diagnostics: Vec::new(),
+ };
-impl DiagnosticSource for BibSyntaxErrors {
- fn update(&mut self, _workspace: &Workspace, document: &Document) {
- let mut analyzer = Analyzer {
- document,
- diagnostics: Vec::new(),
- };
+ analyzer.analyze_root();
- analyzer.analyze_root();
- self.0
- .errors
- .insert(document.uri.clone(), analyzer.diagnostics);
- }
+ *results
+ .entry(document.uri.clone())
+ .or_insert_vec(Vec::new()) = analyzer.diagnostics;
- fn publish<'db>(
- &'db mut self,
- workspace: &'db Workspace,
- builder: &mut DiagnosticBuilder<'db>,
- ) {
- self.0.publish(workspace, builder);
- }
+ Some(())
}
struct Analyzer<'a> {
- document: &'a Document,
+ data: &'a BibDocumentData,
diagnostics: Vec<Diagnostic>,
}
impl<'a> Analyzer<'a> {
fn analyze_root(&mut self) {
- let DocumentData::Bib(data) = &self.document.data else { return };
-
- for node in bibtex::SyntaxNode::new_root(data.green.clone()).descendants() {
+ for node in self.data.root_node().descendants() {
if let Some(entry) = bibtex::Entry::cast(node.clone()) {
self.analyze_entry(entry);
} else if let Some(field) = bibtex::Field::cast(node.clone()) {
@@ -54,50 +41,50 @@ impl<'a> Analyzer<'a> {
fn analyze_entry(&mut self, entry: bibtex::Entry) {
if entry.left_delim_token().is_none() {
let offset = entry.type_token().unwrap().text_range().end();
- self.diagnostics.push(Diagnostic {
- range: TextRange::empty(offset),
- data: DiagnosticData::Bib(BibError::ExpectingLCurly),
- });
+ self.diagnostics.push(Diagnostic::Bib(
+ TextRange::empty(offset),
+ BibError::ExpectingLCurly,
+ ));
return;
}
if entry.name_token().is_none() {
let offset = entry.left_delim_token().unwrap().text_range().end();
- self.diagnostics.push(Diagnostic {
- range: TextRange::empty(offset),
- data: DiagnosticData::Bib(BibError::ExpectingKey),
- });
+ self.diagnostics.push(Diagnostic::Bib(
+ TextRange::empty(offset),
+ BibError::ExpectingKey,
+ ));
return;
}
if entry.right_delim_token().is_none() {
let offset = entry.syntax().text_range().end();
- self.diagnostics.push(Diagnostic {
- range: TextRange::empty(offset),
- data: DiagnosticData::Bib(BibError::ExpectingRCurly),
- });
+ self.diagnostics.push(Diagnostic::Bib(
+ TextRange::empty(offset),
+ BibError::ExpectingRCurly,
+ ));
}
}
fn analyze_field(&mut self, field: bibtex::Field) {
if field.eq_token().is_none() {
let offset = field.name_token().unwrap().text_range().end();
- self.diagnostics.push(Diagnostic {
- range: TextRange::empty(offset),
- data: DiagnosticData::Bib(BibError::ExpectingEq),
- });
+ self.diagnostics.push(Diagnostic::Bib(
+ TextRange::empty(offset),
+ BibError::ExpectingEq,
+ ));
return;
}
if field.value().is_none() {
let offset = field.eq_token().unwrap().text_range().end();
- self.diagnostics.push(Diagnostic {
- range: TextRange::empty(offset),
- data: DiagnosticData::Bib(BibError::ExpectingFieldValue),
- });
+ self.diagnostics.push(Diagnostic::Bib(
+ TextRange::empty(offset),
+ BibError::ExpectingFieldValue,
+ ));
}
}
}
diff --git a/support/texlab/crates/diagnostics/src/grammar/tex.rs b/support/texlab/crates/diagnostics/src/grammar/tex.rs
index b61ae4b020..fb2fa5b5fd 100644
--- a/support/texlab/crates/diagnostics/src/grammar/tex.rs
+++ b/support/texlab/crates/diagnostics/src/grammar/tex.rs
@@ -1,56 +1,47 @@
-use base_db::{Config, Document, DocumentData, Workspace};
+use base_db::{Config, Document, TexDocumentData};
+use multimap::MultiMap;
use rowan::{ast::AstNode, NodeOrToken, TextRange};
use syntax::latex;
-
-use crate::{
- types::{DiagnosticData, TexError},
- util::SimpleDiagnosticSource,
- Diagnostic, DiagnosticBuilder, DiagnosticSource,
-};
-
-#[derive(Default)]
-pub struct TexSyntaxErrors(SimpleDiagnosticSource);
-
-impl DiagnosticSource for TexSyntaxErrors {
- fn update(&mut self, workspace: &Workspace, document: &Document) {
- let mut analyzer = Analyzer {
- document,
- config: workspace.config(),
- diagnostics: Vec::new(),
- };
-
- analyzer.analyze_root();
- self.0
- .errors
- .insert(document.uri.clone(), analyzer.diagnostics);
+use url::Url;
+
+use crate::types::{Diagnostic, TexError};
+
+pub fn update(
+ document: &Document,
+ config: &Config,
+ results: &mut MultiMap<Url, Diagnostic>,
+) -> Option<()> {
+ let data = document.data.as_tex()?;
+ if !document.uri.as_str().ends_with(".tex") {
+ return None;
}
- fn publish<'db>(
- &'db mut self,
- workspace: &'db Workspace,
- builder: &mut DiagnosticBuilder<'db>,
- ) {
- self.0.publish(workspace, builder);
- }
+ let mut analyzer = Analyzer {
+ data,
+ config,
+ diagnostics: Vec::new(),
+ };
+
+ analyzer.analyze_root();
+
+ *results
+ .entry(document.uri.clone())
+ .or_insert_vec(Vec::new()) = analyzer.diagnostics;
+
+ Some(())
}
struct Analyzer<'a> {
- document: &'a Document,
+ data: &'a TexDocumentData,
config: &'a Config,
diagnostics: Vec<Diagnostic>,
}
impl<'a> Analyzer<'a> {
fn analyze_root(&mut self) {
- if !self.document.uri.as_str().ends_with(".tex") {
- return;
- }
-
- let DocumentData::Tex(data) = &self.document.data else { return };
-
let verbatim_envs = &self.config.syntax.verbatim_environments;
- let mut traversal = latex::SyntaxNode::new_root(data.green.clone()).preorder();
+ let mut traversal = self.data.root_node().preorder();
while let Some(event) = traversal.next() {
match event {
rowan::WalkEvent::Enter(node) => {
@@ -82,10 +73,10 @@ impl<'a> Analyzer<'a> {
let begin = environment.begin()?.name()?.key()?;
let end = environment.end()?.name()?.key()?;
if begin != end {
- self.diagnostics.push(Diagnostic {
- range: latex::small_range(&begin),
- data: DiagnosticData::Tex(TexError::MismatchedEnvironment),
- });
+ self.diagnostics.push(Diagnostic::Tex(
+ latex::small_range(&begin),
+ TexError::MismatchedEnvironment,
+ ));
}
Some(())
@@ -108,10 +99,10 @@ impl<'a> Analyzer<'a> {
.filter_map(NodeOrToken::into_token)
.any(|token| token.kind() == latex::R_CURLY)
{
- self.diagnostics.push(Diagnostic {
- range: TextRange::empty(node.text_range().end()),
- data: DiagnosticData::Tex(TexError::ExpectingRCurly),
- });
+ self.diagnostics.push(Diagnostic::Tex(
+ TextRange::empty(node.text_range().end()),
+ TexError::ExpectingRCurly,
+ ));
}
Some(())
@@ -119,10 +110,10 @@ impl<'a> Analyzer<'a> {
fn analyze_curly_braces(&mut self, node: latex::SyntaxNode) -> Option<()> {
if node.kind() == latex::ERROR && node.first_token()?.text() == "}" {
- self.diagnostics.push(Diagnostic {
- range: node.text_range(),
- data: DiagnosticData::Tex(TexError::UnexpectedRCurly),
- });
+ self.diagnostics.push(Diagnostic::Tex(
+ node.text_range(),
+ TexError::UnexpectedRCurly,
+ ));
Some(())
} else {
diff --git a/support/texlab/crates/diagnostics/src/labels.rs b/support/texlab/crates/diagnostics/src/labels.rs
index 03df664a15..92f9072918 100644
--- a/support/texlab/crates/diagnostics/src/labels.rs
+++ b/support/texlab/crates/diagnostics/src/labels.rs
@@ -1,35 +1,18 @@
-use std::borrow::Cow;
-
use base_db::{
semantics::tex::{Label, LabelKind},
util::queries,
DocumentData, Workspace,
};
use itertools::Itertools;
+use multimap::MultiMap;
use rustc_hash::FxHashSet;
+use url::Url;
-use crate::{
- types::{DiagnosticData, TexError},
- Diagnostic, DiagnosticBuilder, DiagnosticSource,
-};
-
-#[derive(Default)]
-pub struct LabelErrors;
-
-impl DiagnosticSource for LabelErrors {
- fn publish<'db>(
- &'db mut self,
- workspace: &'db Workspace,
- builder: &mut DiagnosticBuilder<'db>,
- ) {
- detect_undefined_and_unused_labels(workspace, builder);
- detect_duplicate_labels(workspace, builder);
- }
-}
+use crate::types::{Diagnostic, TexError};
-fn detect_undefined_and_unused_labels<'db>(
- workspace: &'db Workspace,
- builder: &mut DiagnosticBuilder<'db>,
+pub fn detect_undefined_and_unused_labels(
+ workspace: &Workspace,
+ results: &mut MultiMap<Url, Diagnostic>,
) {
let graphs: Vec<_> = workspace
.iter()
@@ -61,25 +44,19 @@ fn detect_undefined_and_unused_labels<'db>(
for label in &data.semantics.labels {
if label.kind != LabelKind::Definition && !label_defs.contains(&label.name.text) {
- let diagnostic = Diagnostic {
- range: label.name.range,
- data: DiagnosticData::Tex(TexError::UndefinedLabel),
- };
- builder.push(&document.uri, Cow::Owned(diagnostic));
+ let diagnostic = Diagnostic::Tex(label.name.range, TexError::UndefinedLabel);
+ results.insert(document.uri.clone(), diagnostic);
}
if label.kind == LabelKind::Definition && !label_refs.contains(&label.name.text) {
- let diagnostic = Diagnostic {
- range: label.name.range,
- data: DiagnosticData::Tex(TexError::UnusedLabel),
- };
- builder.push(&document.uri, Cow::Owned(diagnostic));
+ let diagnostic = Diagnostic::Tex(label.name.range, TexError::UnusedLabel);
+ results.insert(document.uri.clone(), diagnostic);
}
}
}
}
-fn detect_duplicate_labels<'db>(workspace: &'db Workspace, builder: &mut DiagnosticBuilder<'db>) {
+pub fn detect_duplicate_labels(workspace: &Workspace, results: &mut MultiMap<Url, Diagnostic>) {
for conflict in queries::Conflict::find_all::<Label>(workspace) {
let others = conflict
.rest
@@ -87,11 +64,7 @@ fn detect_duplicate_labels<'db>(workspace: &'db Workspace, builder: &mut Diagnos
.map(|location| (location.document.uri.clone(), location.range))
.collect();
- let diagnostic = Diagnostic {
- range: conflict.main.range,
- data: DiagnosticData::Tex(TexError::DuplicateLabel(others)),
- };
-
- builder.push(&conflict.main.document.uri, Cow::Owned(diagnostic));
+ let diagnostic = Diagnostic::Tex(conflict.main.range, TexError::DuplicateLabel(others));
+ results.insert(conflict.main.document.uri.clone(), diagnostic);
}
}
diff --git a/support/texlab/crates/diagnostics/src/lib.rs b/support/texlab/crates/diagnostics/src/lib.rs
index d0be31a0ba..e5baf9ebdb 100644
--- a/support/texlab/crates/diagnostics/src/lib.rs
+++ b/support/texlab/crates/diagnostics/src/lib.rs
@@ -1,88 +1,13 @@
mod build_log;
+pub mod chktex;
mod citations;
mod grammar;
mod labels;
-pub mod types;
-pub(crate) mod util;
+mod manager;
+mod types;
-use std::borrow::Cow;
-
-use base_db::{Document, Workspace};
-use build_log::BuildErrors;
-use citations::CitationErrors;
-use grammar::{BibSyntaxErrors, TexSyntaxErrors};
-use labels::LabelErrors;
-use rustc_hash::FxHashMap;
-use types::Diagnostic;
-use url::Url;
-
-#[derive(Debug, PartialEq, Eq, Clone, Default)]
-pub struct DiagnosticBuilder<'db> {
- inner: FxHashMap<&'db Url, Vec<Cow<'db, Diagnostic>>>,
-}
-
-impl<'db> DiagnosticBuilder<'db> {
- pub fn push(&mut self, uri: &'db Url, diagnostic: Cow<'db, Diagnostic>) {
- self.inner.entry(uri).or_default().push(diagnostic);
- }
-
- pub fn push_many(
- &mut self,
- uri: &'db Url,
- diagnostics: impl Iterator<Item = Cow<'db, Diagnostic>>,
- ) {
- self.inner.entry(uri).or_default().extend(diagnostics);
- }
-
- pub fn iter(&self) -> impl Iterator<Item = (&'db Url, impl Iterator<Item = &Diagnostic>)> {
- self.inner
- .iter()
- .map(|(uri, diagnostics)| (*uri, diagnostics.iter().map(|diag| diag.as_ref())))
- }
-}
-
-pub trait DiagnosticSource {
- #[allow(unused_variables)]
- fn update(&mut self, workspace: &Workspace, document: &Document) {}
-
- fn publish<'db>(&'db mut self, workspace: &'db Workspace, builder: &mut DiagnosticBuilder<'db>);
-}
-
-pub struct DiagnosticManager {
- sources: Vec<Box<dyn DiagnosticSource>>,
-}
-
-impl Default for DiagnosticManager {
- fn default() -> Self {
- let sources: Vec<Box<dyn DiagnosticSource>> = vec![
- Box::<TexSyntaxErrors>::default(),
- Box::<BibSyntaxErrors>::default(),
- Box::<BuildErrors>::default(),
- Box::<LabelErrors>::default(),
- Box::<CitationErrors>::default(),
- ];
-
- Self { sources }
- }
-}
-
-impl DiagnosticSource for DiagnosticManager {
- fn update(&mut self, workspace: &Workspace, document: &Document) {
- for source in &mut self.sources {
- source.update(workspace, document);
- }
- }
-
- fn publish<'db>(
- &'db mut self,
- workspace: &'db Workspace,
- builder: &mut DiagnosticBuilder<'db>,
- ) {
- for source in &mut self.sources {
- source.publish(workspace, builder);
- }
- }
-}
+pub use manager::Manager;
+pub use types::*;
#[cfg(test)]
mod tests;
diff --git a/support/texlab/crates/diagnostics/src/manager.rs b/support/texlab/crates/diagnostics/src/manager.rs
new file mode 100644
index 0000000000..1ce5e194b8
--- /dev/null
+++ b/support/texlab/crates/diagnostics/src/manager.rs
@@ -0,0 +1,75 @@
+use base_db::{util::filter_regex_patterns, Document, Owner, Workspace};
+use multimap::MultiMap;
+use rustc_hash::FxHashMap;
+use url::Url;
+
+use crate::types::Diagnostic;
+
+/// Manages all diagnostics for a workspace.
+#[derive(Debug, Default)]
+pub struct Manager {
+ grammar: MultiMap<Url, Diagnostic>,
+ chktex: FxHashMap<Url, Vec<Diagnostic>>,
+ build_log: FxHashMap<Url, MultiMap<Url, Diagnostic>>,
+}
+
+impl Manager {
+ /// Updates the syntax-based diagnostics for the given document.
+ pub fn update_syntax(&mut self, workspace: &Workspace, document: &Document) {
+ self.grammar.remove(&document.uri);
+ super::grammar::tex::update(document, workspace.config(), &mut self.grammar);
+ super::grammar::bib::update(document, &mut self.grammar);
+
+ self.build_log.remove(&document.uri);
+ super::build_log::update(workspace, document, &mut self.build_log);
+ }
+
+ /// Updates the ChkTeX diagnostics for the given document.
+ pub fn update_chktex(&mut self, uri: Url, diagnostics: Vec<Diagnostic>) {
+ self.chktex.insert(uri, diagnostics);
+ }
+
+ /// Returns all filtered diagnostics for the given workspace.
+ pub fn get(&self, workspace: &Workspace) -> MultiMap<Url, Diagnostic> {
+ let mut results = MultiMap::default();
+ for (uri, diagnostics) in &self.grammar {
+ results.insert_many_from_slice(uri.clone(), diagnostics);
+ }
+
+ for (uri, diagnostics) in self.build_log.values().flatten() {
+ results.insert_many_from_slice(uri.clone(), diagnostics);
+ }
+
+ for (uri, diagnostics) in &self.chktex {
+ if workspace
+ .lookup(uri)
+ .map_or(false, |document| document.owner == Owner::Client)
+ {
+ results.insert_many_from_slice(uri.clone(), diagnostics);
+ }
+ }
+
+ for document in workspace.iter() {
+ let project = workspace.project(document);
+ super::citations::detect_undefined_citations(&project, document, &mut results);
+ super::citations::detect_unused_entries(&project, document, &mut results);
+ }
+
+ super::citations::detect_duplicate_entries(workspace, &mut results);
+ super::labels::detect_duplicate_labels(workspace, &mut results);
+ super::labels::detect_undefined_and_unused_labels(workspace, &mut results);
+
+ let config = &workspace.config().diagnostics;
+ for (_, diagnostics) in &mut results {
+ diagnostics.retain(|diagnostic| {
+ filter_regex_patterns(
+ &diagnostic.message(),
+ &config.allowed_patterns,
+ &config.ignored_patterns,
+ )
+ });
+ }
+
+ results
+ }
+}
diff --git a/support/texlab/crates/diagnostics/src/tests.rs b/support/texlab/crates/diagnostics/src/tests.rs
index 539e3ba765..b7c6004d19 100644
--- a/support/texlab/crates/diagnostics/src/tests.rs
+++ b/support/texlab/crates/diagnostics/src/tests.rs
@@ -1,37 +1,23 @@
-use std::borrow::Cow;
+use expect_test::{expect, Expect};
+use itertools::Itertools;
-use crate::{
- types::{BibError, Diagnostic, DiagnosticData, TexError},
- DiagnosticBuilder, DiagnosticManager, DiagnosticSource,
-};
-
-fn check(input: &str, expected_data: &[DiagnosticData]) {
+fn check(input: &str, expect: Expect) {
let fixture = test_utils::fixture::Fixture::parse(input);
- let mut manager = DiagnosticManager::default();
-
- let mut expected = DiagnosticBuilder::default();
- let mut expected_data = expected_data.iter();
- for document in &fixture.documents {
- let diagnostics = document.ranges.iter().copied().map(|range| {
- let data = expected_data.next().unwrap().clone();
- Cow::Owned(Diagnostic { range, data })
- });
-
- expected.push_many(&document.uri, diagnostics);
- }
+ let mut manager = crate::Manager::default();
for document in fixture.workspace.iter() {
- manager.update(&fixture.workspace, &document);
+ manager.update_syntax(&fixture.workspace, &document);
}
- let mut actual = DiagnosticBuilder::default();
- manager.publish(&fixture.workspace, &mut actual);
-
- for diagnostics in actual.inner.values_mut() {
- diagnostics.sort_by_key(|diag| (diag.range.start(), diag.range.len()));
- }
+ let results = manager.get(&fixture.workspace);
+ let results = results
+ .iter_all()
+ .filter(|(_, diags)| !diags.is_empty())
+ .sorted_by(|(uri1, _), (uri2, _)| uri1.cmp(&uri2))
+ .map(|(uri, diags)| (uri.as_str(), diags))
+ .collect_vec();
- assert_eq!(actual, expected);
+ expect.assert_debug_eq(&results);
}
#[test]
@@ -42,7 +28,19 @@ fn test_bib_entry_missing_l_delim() {
@article
!
"#,
- &[DiagnosticData::Bib(BibError::ExpectingLCurly)],
+ expect![[r#"
+ [
+ (
+ "file:///texlab/main.bib",
+ [
+ Bib(
+ 8..8,
+ ExpectingLCurly,
+ ),
+ ],
+ ),
+ ]
+ "#]],
)
}
@@ -58,7 +56,19 @@ fn test_bib_entry_missing_r_delim() {
\bibliography{main}
\cite{foo}
"#,
- &[DiagnosticData::Bib(BibError::ExpectingRCurly)],
+ expect![[r#"
+ [
+ (
+ "file:///texlab/main.bib",
+ [
+ Bib(
+ 14..14,
+ ExpectingRCurly,
+ ),
+ ],
+ ),
+ ]
+ "#]],
)
}
@@ -69,7 +79,19 @@ fn test_bib_entry_missing_name() {
%! main.bib
@article{
!"#,
- &[DiagnosticData::Bib(BibError::ExpectingKey)],
+ expect![[r#"
+ [
+ (
+ "file:///texlab/main.bib",
+ [
+ Bib(
+ 9..9,
+ ExpectingKey,
+ ),
+ ],
+ ),
+ ]
+ "#]],
)
}
@@ -87,7 +109,19 @@ fn test_bib_field_missing_eq() {
\bibliography{main}
\cite{foo}
"#,
- &[DiagnosticData::Bib(BibError::ExpectingEq)],
+ expect![[r#"
+ [
+ (
+ "file:///texlab/main.bib",
+ [
+ Bib(
+ 23..23,
+ ExpectingEq,
+ ),
+ ],
+ ),
+ ]
+ "#]],
)
}
@@ -105,7 +139,19 @@ fn test_bib_field_missing_value() {
\bibliography{main}
\cite{foo}
"#,
- &[DiagnosticData::Bib(BibError::ExpectingFieldValue)],
+ expect![[r#"
+ [
+ (
+ "file:///texlab/main.bib",
+ [
+ Bib(
+ 25..25,
+ ExpectingFieldValue,
+ ),
+ ],
+ ),
+ ]
+ "#]],
)
}
@@ -119,10 +165,23 @@ fn test_tex_unmatched_braces() {
{
!
"#,
- &[
- DiagnosticData::Tex(TexError::UnexpectedRCurly),
- DiagnosticData::Tex(TexError::ExpectingRCurly),
- ],
+ expect![[r#"
+ [
+ (
+ "file:///texlab/main.tex",
+ [
+ Tex(
+ 0..1,
+ UnexpectedRCurly,
+ ),
+ Tex(
+ 4..4,
+ ExpectingRCurly,
+ ),
+ ],
+ ),
+ ]
+ "#]],
)
}
@@ -135,7 +194,19 @@ fn test_tex_environment_mismatched() {
^^^
\end{bar}
"#,
- &[DiagnosticData::Tex(TexError::MismatchedEnvironment)],
+ expect![[r#"
+ [
+ (
+ "file:///texlab/main.tex",
+ [
+ Tex(
+ 7..10,
+ MismatchedEnvironment,
+ ),
+ ],
+ ),
+ ]
+ "#]],
)
}
@@ -148,7 +219,19 @@ fn test_label_unused() {
^^^
\label{bar}\ref{bar}
"#,
- &[DiagnosticData::Tex(TexError::UnusedLabel)],
+ expect![[r#"
+ [
+ (
+ "file:///texlab/main.tex",
+ [
+ Tex(
+ 7..10,
+ UnusedLabel,
+ ),
+ ],
+ ),
+ ]
+ "#]],
)
}
@@ -160,7 +243,19 @@ fn test_label_undefined() {
\ref{foo}
^^^
"#,
- &[DiagnosticData::Tex(TexError::UndefinedLabel)],
+ expect![[r#"
+ [
+ (
+ "file:///texlab/main.tex",
+ [
+ Tex(
+ 5..8,
+ UndefinedLabel,
+ ),
+ ],
+ ),
+ ]
+ "#]],
)
}
@@ -172,7 +267,19 @@ fn test_citation_undefined() {
\cite{foo}
^^^
"#,
- &[DiagnosticData::Tex(TexError::UndefinedCitation)],
+ expect![[r#"
+ [
+ (
+ "file:///texlab/main.tex",
+ [
+ Tex(
+ 6..9,
+ UndefinedCitation,
+ ),
+ ],
+ ),
+ ]
+ "#]],
)
}
@@ -184,6 +291,18 @@ fn test_citation_unused() {
@article{foo,}
^^^
"#,
- &[DiagnosticData::Bib(BibError::UnusedEntry)],
+ expect![[r#"
+ [
+ (
+ "file:///texlab/main.bib",
+ [
+ Bib(
+ 9..12,
+ UnusedEntry,
+ ),
+ ],
+ ),
+ ]
+ "#]],
)
}
diff --git a/support/texlab/crates/diagnostics/src/types.rs b/support/texlab/crates/diagnostics/src/types.rs
index a443245b6f..9cbbf8936f 100644
--- a/support/texlab/crates/diagnostics/src/types.rs
+++ b/support/texlab/crates/diagnostics/src/types.rs
@@ -1,21 +1,9 @@
+use line_index::LineCol;
use rowan::TextRange;
use syntax::BuildError;
use url::Url;
#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct Diagnostic {
- pub range: TextRange,
- pub data: DiagnosticData,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub enum DiagnosticData {
- Tex(TexError),
- Bib(BibError),
- Build(BuildError),
-}
-
-#[derive(Debug, PartialEq, Eq, Clone)]
pub enum TexError {
UnexpectedRCurly,
ExpectingRCurly,
@@ -36,3 +24,54 @@ pub enum BibError {
UnusedEntry,
DuplicateEntry(Vec<(Url, TextRange)>),
}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct ChktexError {
+ pub start: LineCol,
+ pub end: LineCol,
+ pub message: String,
+ pub severity: ChktexSeverity,
+ pub code: String,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum ChktexSeverity {
+ Error,
+ Warning,
+ Message,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum Diagnostic {
+ Tex(TextRange, TexError),
+ Bib(TextRange, BibError),
+ Build(TextRange, BuildError),
+ Chktex(ChktexError),
+}
+
+impl Diagnostic {
+ pub fn message(&self) -> &str {
+ match self {
+ Diagnostic::Tex(_, error) => match error {
+ TexError::UnexpectedRCurly => "Unexpected \"}\"",
+ TexError::ExpectingRCurly => "Expecting a curly bracket: \"}\"",
+ TexError::MismatchedEnvironment => "Mismatched environment",
+ TexError::UnusedLabel => "Unused label",
+ TexError::UndefinedLabel => "Undefined reference",
+ TexError::UndefinedCitation => "Undefined reference",
+ TexError::DuplicateLabel(_) => "Duplicate label",
+ },
+ Diagnostic::Bib(_, error) => match error {
+ BibError::ExpectingLCurly => "Expecting a curly bracket: \"{\"",
+ BibError::ExpectingKey => "Expecting a key",
+ BibError::ExpectingRCurly => "Expecting a curly bracket: \"}\"",
+ BibError::ExpectingEq => "Expecting an equality sign: \"=\"",
+ BibError::ExpectingFieldValue => "Expecting a field value",
+ BibError::UnusedEntry => "Unused entry",
+ BibError::DuplicateEntry(_) => "Duplicate entry key",
+ },
+ Diagnostic::Build(_, error) => &error.message,
+ Diagnostic::Chktex(error) => &error.message,
+ }
+ }
+}
diff --git a/support/texlab/crates/diagnostics/src/util.rs b/support/texlab/crates/diagnostics/src/util.rs
deleted file mode 100644
index fd34125dbe..0000000000
--- a/support/texlab/crates/diagnostics/src/util.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-use std::borrow::Cow;
-
-use base_db::Workspace;
-use rustc_hash::FxHashMap;
-use url::Url;
-
-use crate::{Diagnostic, DiagnosticBuilder, DiagnosticSource};
-
-#[derive(Default)]
-pub struct SimpleDiagnosticSource {
- pub errors: FxHashMap<Url, Vec<Diagnostic>>,
-}
-
-impl DiagnosticSource for SimpleDiagnosticSource {
- fn publish<'db>(
- &'db mut self,
- workspace: &'db Workspace,
- builder: &mut DiagnosticBuilder<'db>,
- ) {
- self.errors.retain(|uri, _| workspace.lookup(uri).is_some());
-
- for document in workspace.iter() {
- if let Some(diagnostics) = self.errors.get(&document.uri) {
- builder.push_many(&document.uri, diagnostics.iter().map(Cow::Borrowed));
- }
- }
- }
-}