summaryrefslogtreecommitdiff
path: root/support/texlab/crates/diagnostics
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2023-07-31 03:02:46 +0000
committerNorbert Preining <norbert@preining.info>2023-07-31 03:02:46 +0000
commit17d547a1effe2cafcdfbf704bdf8cb0484790ef2 (patch)
tree30556f1ebbb411da2d3e7297c0c4a2ffbd5af4ee /support/texlab/crates/diagnostics
parent595d37aac232836c0519c45f2078c5272122eb32 (diff)
CTAN sync 202307310302
Diffstat (limited to 'support/texlab/crates/diagnostics')
-rw-r--r--support/texlab/crates/diagnostics/Cargo.toml21
-rw-r--r--support/texlab/crates/diagnostics/src/build_log.rs101
-rw-r--r--support/texlab/crates/diagnostics/src/citations.rs97
-rw-r--r--support/texlab/crates/diagnostics/src/grammar.rs4
-rw-r--r--support/texlab/crates/diagnostics/src/grammar/bib.rs103
-rw-r--r--support/texlab/crates/diagnostics/src/grammar/tex.rs132
-rw-r--r--support/texlab/crates/diagnostics/src/labels.rs97
-rw-r--r--support/texlab/crates/diagnostics/src/lib.rs86
-rw-r--r--support/texlab/crates/diagnostics/src/tests.rs191
-rw-r--r--support/texlab/crates/diagnostics/src/types.rs38
-rw-r--r--support/texlab/crates/diagnostics/src/util.rs28
11 files changed, 898 insertions, 0 deletions
diff --git a/support/texlab/crates/diagnostics/Cargo.toml b/support/texlab/crates/diagnostics/Cargo.toml
new file mode 100644
index 0000000000..3f87c8c879
--- /dev/null
+++ b/support/texlab/crates/diagnostics/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "diagnostics"
+version = "0.0.0"
+license.workspace = true
+authors.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+base-db = { path = "../base-db" }
+itertools = "0.11.0"
+rowan = "0.15.11"
+rustc-hash = "1.1.0"
+syntax = { path = "../syntax" }
+url = "=2.3.1"
+
+[dev-dependencies]
+test-utils = { path = "../test-utils" }
+
+[lib]
+doctest = false
diff --git a/support/texlab/crates/diagnostics/src/build_log.rs b/support/texlab/crates/diagnostics/src/build_log.rs
new file mode 100644
index 0000000000..f5770c90fb
--- /dev/null
+++ b/support/texlab/crates/diagnostics/src/build_log.rs
@@ -0,0 +1,101 @@
+use std::borrow::Cow;
+
+use base_db::{Document, Workspace};
+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>>,
+}
+
+#[derive(Debug, Default)]
+pub struct BuildErrors {
+ logs: FxHashMap<Url, BuildLog>,
+}
+
+impl DiagnosticSource for BuildErrors {
+ fn update(&mut self, workspace: &Workspace, log_document: &Document) {
+ let mut errors: FxHashMap<Url, Vec<Diagnostic>> = FxHashMap::default();
+
+ let Some(data) = log_document.data.as_log() else { return };
+
+ let parents = workspace.parents(log_document);
+ let Some(root_document) = parents.iter().next() else { return };
+
+ 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 };
+ 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
+ .newlines
+ .get(line as usize)
+ .unwrap_or(&TextSize::from(0));
+
+ 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 });
+ }
+
+ 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));
+ }
+ }
+ }
+}
+
+fn find_range_of_hint(document: &Document, error: &BuildError) -> Option<TextRange> {
+ let line = error.line? as usize;
+ let hint = error.hint.as_deref()?;
+ let line_index = &document.line_index;
+
+ let line_start = line_index.newlines.get(line).copied()?;
+ let line_end = line_index
+ .newlines
+ .get(line + 1)
+ .copied()
+ .unwrap_or((&document.text).text_len());
+
+ let line_text = &document.text[line_start.into()..line_end.into()];
+ let hint_start = line_start + TextSize::try_from(line_text.find(hint)?).unwrap();
+ let hint_end = hint_start + hint.text_len();
+ Some(TextRange::new(hint_start, hint_end))
+}
diff --git a/support/texlab/crates/diagnostics/src/citations.rs b/support/texlab/crates/diagnostics/src/citations.rs
new file mode 100644
index 0000000000..e6b4d1022a
--- /dev/null
+++ b/support/texlab/crates/diagnostics/src/citations.rs
@@ -0,0 +1,97 @@
+use std::borrow::Cow;
+
+use base_db::{
+ semantics::{bib::Entry, tex::Citation},
+ util::queries::{self, Object},
+ BibDocumentData, Document, DocumentData, Project, TexDocumentData, Workspace,
+};
+use rustc_hash::FxHashSet;
+
+use crate::{
+ types::{BibError, Diagnostic, DiagnosticData, TexError},
+ DiagnosticBuilder, DiagnosticSource,
+};
+
+#[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);
+
+ if let DocumentData::Tex(data) = &document.data {
+ detect_undefined_citations(&project, document, data, builder);
+ } else if let DocumentData::Bib(data) = &document.data {
+ detect_unused_entries(&project, document, data, builder);
+ }
+ }
+
+ detect_duplicate_entries(workspace, builder);
+ }
+}
+
+fn detect_undefined_citations<'db>(
+ project: &Project<'db>,
+ document: &'db Document,
+ data: &TexDocumentData,
+ builder: &mut DiagnosticBuilder<'db>,
+) {
+ let entries: FxHashSet<&str> = Entry::find_all(project)
+ .map(|(_, entry)| entry.name_text())
+ .collect();
+
+ for citation in &data.semantics.citations {
+ if !entries.contains(citation.name.text.as_str()) {
+ let diagnostic = Diagnostic {
+ range: citation.name.range,
+ data: DiagnosticData::Tex(TexError::UndefinedCitation),
+ };
+
+ builder.push(&document.uri, Cow::Owned(diagnostic));
+ }
+ }
+}
+
+fn detect_unused_entries<'db>(
+ project: &Project<'db>,
+ document: &'db Document,
+ data: &BibDocumentData,
+ builder: &mut DiagnosticBuilder<'db>,
+) {
+ let citations: FxHashSet<&str> = Citation::find_all(project)
+ .map(|(_, citation)| citation.name_text())
+ .collect();
+
+ 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));
+ }
+ }
+}
+
+fn detect_duplicate_entries<'db>(workspace: &'db Workspace, builder: &mut DiagnosticBuilder<'db>) {
+ for conflict in queries::Conflict::find_all::<Entry>(workspace) {
+ let others = conflict
+ .rest
+ .iter()
+ .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));
+ }
+}
diff --git a/support/texlab/crates/diagnostics/src/grammar.rs b/support/texlab/crates/diagnostics/src/grammar.rs
new file mode 100644
index 0000000000..1a9e3cecda
--- /dev/null
+++ b/support/texlab/crates/diagnostics/src/grammar.rs
@@ -0,0 +1,4 @@
+mod bib;
+mod tex;
+
+pub use self::{bib::BibSyntaxErrors, tex::TexSyntaxErrors};
diff --git a/support/texlab/crates/diagnostics/src/grammar/bib.rs b/support/texlab/crates/diagnostics/src/grammar/bib.rs
new file mode 100644
index 0000000000..e101e68f2a
--- /dev/null
+++ b/support/texlab/crates/diagnostics/src/grammar/bib.rs
@@ -0,0 +1,103 @@
+use base_db::{Document, DocumentData, Workspace};
+use rowan::{ast::AstNode, TextRange};
+use syntax::bibtex::{self, HasDelims, HasEq, HasName, HasType, HasValue};
+
+use crate::{
+ types::{BibError, DiagnosticData},
+ util::SimpleDiagnosticSource,
+ Diagnostic, DiagnosticBuilder, DiagnosticSource,
+};
+
+#[derive(Default)]
+pub struct BibSyntaxErrors(SimpleDiagnosticSource);
+
+impl DiagnosticSource for BibSyntaxErrors {
+ fn update(&mut self, _workspace: &Workspace, document: &Document) {
+ let mut analyzer = Analyzer {
+ document,
+ diagnostics: Vec::new(),
+ };
+
+ analyzer.analyze_root();
+ self.0
+ .errors
+ .insert(document.uri.clone(), analyzer.diagnostics);
+ }
+
+ fn publish<'db>(
+ &'db mut self,
+ workspace: &'db Workspace,
+ builder: &mut DiagnosticBuilder<'db>,
+ ) {
+ self.0.publish(workspace, builder);
+ }
+}
+
+struct Analyzer<'a> {
+ document: &'a Document,
+ 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() {
+ if let Some(entry) = bibtex::Entry::cast(node.clone()) {
+ self.analyze_entry(entry);
+ } else if let Some(field) = bibtex::Field::cast(node.clone()) {
+ self.analyze_field(field);
+ }
+ }
+ }
+
+ 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),
+ });
+
+ 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),
+ });
+
+ 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),
+ });
+ }
+ }
+
+ 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),
+ });
+
+ 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),
+ });
+ }
+ }
+}
diff --git a/support/texlab/crates/diagnostics/src/grammar/tex.rs b/support/texlab/crates/diagnostics/src/grammar/tex.rs
new file mode 100644
index 0000000000..b61ae4b020
--- /dev/null
+++ b/support/texlab/crates/diagnostics/src/grammar/tex.rs
@@ -0,0 +1,132 @@
+use base_db::{Config, Document, DocumentData, Workspace};
+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);
+ }
+
+ fn publish<'db>(
+ &'db mut self,
+ workspace: &'db Workspace,
+ builder: &mut DiagnosticBuilder<'db>,
+ ) {
+ self.0.publish(workspace, builder);
+ }
+}
+
+struct Analyzer<'a> {
+ document: &'a Document,
+ 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();
+ while let Some(event) = traversal.next() {
+ match event {
+ rowan::WalkEvent::Enter(node) => {
+ if let Some(environment) = latex::Environment::cast(node.clone()) {
+ if environment
+ .begin()
+ .and_then(|begin| begin.name())
+ .and_then(|name| name.key())
+ .map_or(false, |name| verbatim_envs.contains(&name.to_string()))
+ {
+ traversal.skip_subtree();
+ continue;
+ }
+ }
+
+ self.analyze_environment(node.clone())
+ .or_else(|| self.analyze_curly_group(node.clone()))
+ .or_else(|| self.analyze_curly_braces(node));
+ }
+ rowan::WalkEvent::Leave(_) => {
+ continue;
+ }
+ };
+ }
+ }
+
+ fn analyze_environment(&mut self, node: latex::SyntaxNode) -> Option<()> {
+ let environment = latex::Environment::cast(node)?;
+ 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),
+ });
+ }
+
+ Some(())
+ }
+
+ fn analyze_curly_group(&mut self, node: latex::SyntaxNode) -> Option<()> {
+ if !matches!(
+ node.kind(),
+ latex::CURLY_GROUP
+ | latex::CURLY_GROUP_COMMAND
+ | latex::CURLY_GROUP_KEY_VALUE
+ | latex::CURLY_GROUP_WORD
+ | latex::CURLY_GROUP_WORD_LIST
+ ) {
+ return None;
+ }
+
+ if !node
+ .children_with_tokens()
+ .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),
+ });
+ }
+
+ Some(())
+ }
+
+ 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),
+ });
+
+ Some(())
+ } else {
+ None
+ }
+ }
+}
diff --git a/support/texlab/crates/diagnostics/src/labels.rs b/support/texlab/crates/diagnostics/src/labels.rs
new file mode 100644
index 0000000000..03df664a15
--- /dev/null
+++ b/support/texlab/crates/diagnostics/src/labels.rs
@@ -0,0 +1,97 @@
+use std::borrow::Cow;
+
+use base_db::{
+ semantics::tex::{Label, LabelKind},
+ util::queries,
+ DocumentData, Workspace,
+};
+use itertools::Itertools;
+use rustc_hash::FxHashSet;
+
+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);
+ }
+}
+
+fn detect_undefined_and_unused_labels<'db>(
+ workspace: &'db Workspace,
+ builder: &mut DiagnosticBuilder<'db>,
+) {
+ let graphs: Vec<_> = workspace
+ .iter()
+ .map(|start| base_db::graph::Graph::new(workspace, start))
+ .collect();
+
+ for document in workspace.iter() {
+ let DocumentData::Tex(data) = &document.data else {
+ continue;
+ };
+
+ let mut label_refs = FxHashSet::default();
+ let mut label_defs = FxHashSet::default();
+ let project = graphs
+ .iter()
+ .filter(|graph| graph.preorder().contains(&document))
+ .flat_map(|graph| graph.preorder());
+
+ for label in project
+ .filter_map(|child| child.data.as_tex())
+ .flat_map(|data| data.semantics.labels.iter())
+ {
+ if label.kind == LabelKind::Definition {
+ label_defs.insert(&label.name.text);
+ } else {
+ label_refs.insert(&label.name.text);
+ }
+ }
+
+ 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));
+ }
+
+ 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));
+ }
+ }
+ }
+}
+
+fn detect_duplicate_labels<'db>(workspace: &'db Workspace, builder: &mut DiagnosticBuilder<'db>) {
+ for conflict in queries::Conflict::find_all::<Label>(workspace) {
+ let others = conflict
+ .rest
+ .iter()
+ .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));
+ }
+}
diff --git a/support/texlab/crates/diagnostics/src/lib.rs b/support/texlab/crates/diagnostics/src/lib.rs
new file mode 100644
index 0000000000..e55ecb78e6
--- /dev/null
+++ b/support/texlab/crates/diagnostics/src/lib.rs
@@ -0,0 +1,86 @@
+mod build_log;
+mod citations;
+mod grammar;
+mod labels;
+pub mod types;
+pub(crate) mod util;
+
+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 mut sources: Vec<Box<dyn DiagnosticSource>> = Vec::new();
+ sources.push(Box::new(TexSyntaxErrors::default()));
+ sources.push(Box::new(BibSyntaxErrors::default()));
+ sources.push(Box::new(BuildErrors::default()));
+ sources.push(Box::new(LabelErrors::default()));
+ sources.push(Box::new(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);
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/support/texlab/crates/diagnostics/src/tests.rs b/support/texlab/crates/diagnostics/src/tests.rs
new file mode 100644
index 0000000000..8b9534f337
--- /dev/null
+++ b/support/texlab/crates/diagnostics/src/tests.rs
@@ -0,0 +1,191 @@
+use std::borrow::Cow;
+
+use test_utils::fixture::Fixture;
+
+use crate::{
+ types::{BibError, Diagnostic, DiagnosticData, TexError},
+ DiagnosticBuilder, DiagnosticManager, DiagnosticSource,
+};
+
+fn check(input: &str, expected_data: &[DiagnosticData]) {
+ let 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);
+ }
+
+ for document in fixture.workspace.iter() {
+ manager.update(&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()));
+ }
+
+ assert_eq!(actual, expected);
+}
+
+#[test]
+fn test_bib_entry_missing_l_delim() {
+ check(
+ r#"
+%! main.bib
+@article
+ !
+"#,
+ &[DiagnosticData::Bib(BibError::ExpectingLCurly)],
+ )
+}
+
+#[test]
+fn test_bib_entry_missing_r_delim() {
+ check(
+ r#"
+%! main.bib
+@article{foo,
+ !
+
+%! main.tex
+\bibliography{main}
+\cite{foo}
+"#,
+ &[DiagnosticData::Bib(BibError::ExpectingRCurly)],
+ )
+}
+
+#[test]
+fn test_bib_entry_missing_name() {
+ check(
+ r#"
+%! main.bib
+@article{
+ !"#,
+ &[DiagnosticData::Bib(BibError::ExpectingKey)],
+ )
+}
+
+#[test]
+fn test_bib_field_missing_eq() {
+ check(
+ r#"
+%! main.bib
+@article{foo,
+ field
+ !
+}
+
+%! main.tex
+\bibliography{main}
+\cite{foo}
+"#,
+ &[DiagnosticData::Bib(BibError::ExpectingEq)],
+ )
+}
+
+#[test]
+fn test_bib_field_missing_value() {
+ check(
+ r#"
+%! main.bib
+@article{foo,
+ field =
+ !
+}
+
+%! main.tex
+\bibliography{main}
+\cite{foo}
+"#,
+ &[DiagnosticData::Bib(BibError::ExpectingFieldValue)],
+ )
+}
+
+#[test]
+fn test_tex_unmatched_braces() {
+ check(
+ r#"
+%! main.tex
+}
+^
+{
+ !
+"#,
+ &[
+ DiagnosticData::Tex(TexError::UnexpectedRCurly),
+ DiagnosticData::Tex(TexError::ExpectingRCurly),
+ ],
+ )
+}
+
+#[test]
+fn test_tex_environment_mismatched() {
+ check(
+ r#"
+%! main.tex
+\begin{foo}
+ ^^^
+\end{bar}
+"#,
+ &[DiagnosticData::Tex(TexError::MismatchedEnvironment)],
+ )
+}
+
+#[test]
+fn test_label_unused() {
+ check(
+ r#"
+%! main.tex
+\label{foo}
+ ^^^
+\label{bar}\ref{bar}
+"#,
+ &[DiagnosticData::Tex(TexError::UnusedLabel)],
+ )
+}
+
+#[test]
+fn test_label_undefined() {
+ check(
+ r#"
+%! main.tex
+\ref{foo}
+ ^^^
+"#,
+ &[DiagnosticData::Tex(TexError::UndefinedLabel)],
+ )
+}
+
+#[test]
+fn test_citation_undefined() {
+ check(
+ r#"
+%! main.tex
+\cite{foo}
+ ^^^
+"#,
+ &[DiagnosticData::Tex(TexError::UndefinedCitation)],
+ )
+}
+
+#[test]
+fn test_citation_unused() {
+ check(
+ r#"
+%! main.bib
+@article{foo,}
+ ^^^
+"#,
+ &[DiagnosticData::Bib(BibError::UnusedEntry)],
+ )
+}
diff --git a/support/texlab/crates/diagnostics/src/types.rs b/support/texlab/crates/diagnostics/src/types.rs
new file mode 100644
index 0000000000..a443245b6f
--- /dev/null
+++ b/support/texlab/crates/diagnostics/src/types.rs
@@ -0,0 +1,38 @@
+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,
+ MismatchedEnvironment,
+ UnusedLabel,
+ UndefinedLabel,
+ UndefinedCitation,
+ DuplicateLabel(Vec<(Url, TextRange)>),
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum BibError {
+ ExpectingLCurly,
+ ExpectingKey,
+ ExpectingRCurly,
+ ExpectingEq,
+ ExpectingFieldValue,
+ UnusedEntry,
+ DuplicateEntry(Vec<(Url, TextRange)>),
+}
diff --git a/support/texlab/crates/diagnostics/src/util.rs b/support/texlab/crates/diagnostics/src/util.rs
new file mode 100644
index 0000000000..fd34125dbe
--- /dev/null
+++ b/support/texlab/crates/diagnostics/src/util.rs
@@ -0,0 +1,28 @@
+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));
+ }
+ }
+ }
+}