From af0217dd409e35b16b38f9394ab0c563e828a12a Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Sat, 4 Jul 2020 12:10:17 +0200 Subject: [PATCH] feat: provides a centralized way to provide diagnostics --- crates/mun_compiler/Cargo.toml | 3 +- crates/mun_compiler/src/annotate.rs | 216 ------- crates/mun_compiler/src/diagnostics.rs | 155 ----- .../mun_compiler/src/diagnostics_snippets.rs | 533 ++++++------------ crates/mun_compiler/src/driver.rs | 76 ++- crates/mun_compiler/src/lib.rs | 3 +- ...cs__tests__access_unknown_field_error.snap | 3 +- ...cs__tests__duplicate_definition_error.snap | 30 +- ...stics__tests__expected_function_error.snap | 22 +- ...nostics__tests__mismatched_type_error.snap | 10 +- ...possibly_uninitialized_variable_error.snap | 5 +- ...ler__diagnostics__tests__syntax_error.snap | 18 +- ...nostics__tests__unresolved_type_error.snap | 8 +- ...ostics__tests__unresolved_value_error.snap | 8 +- crates/mun_diagnostics/Cargo.toml | 2 +- crates/mun_diagnostics/src/hir.rs | 32 +- .../src/hir/access_unknown_field.rs | 26 +- .../src/hir/duplicate_definition_error.rs | 154 +++++ .../src/hir/expected_function.rs | 22 +- .../src/hir/mismatched_type.rs | 25 +- .../mun_diagnostics/src/hir/missing_fields.rs | 28 +- .../src/hir/possibly_unitialized_variable.rs | 23 +- .../src/hir/unresolved_type.rs | 19 +- .../src/hir/unresolved_value.rs | 19 +- crates/mun_diagnostics/src/lib.rs | 39 +- crates/mun_hir/src/lib.rs | 1 + crates/mun_language_server/src/diagnostics.rs | 29 +- crates/mun_language_server/src/main_loop.rs | 83 ++- 28 files changed, 674 insertions(+), 918 deletions(-) delete mode 100644 crates/mun_compiler/src/annotate.rs create mode 100644 crates/mun_diagnostics/src/hir/duplicate_definition_error.rs diff --git a/crates/mun_compiler/Cargo.toml b/crates/mun_compiler/Cargo.toml index d19389300..1b1e80f2f 100644 --- a/crates/mun_compiler/Cargo.toml +++ b/crates/mun_compiler/Cargo.toml @@ -19,7 +19,8 @@ mun_syntax = { version = "=0.2.0", path="../mun_syntax" } mun_hir = { version = "=0.2.0", path="../mun_hir" } mun_target = { version = "=0.2.0", path="../mun_target" } mun_project = { version = "=0.1.0", path = "../mun_project" } -annotate-snippets = { version = "0.6.1", features = ["color"] } +mun_diagnostics = { version = "=0.1.0", path = "../mun_diagnostics" } +annotate-snippets = { version = "0.9.0", features = ["color"] } unicode-segmentation = "1.6.0" ansi_term = "0.12.1" walkdir = "2.3" diff --git a/crates/mun_compiler/src/annotate.rs b/crates/mun_compiler/src/annotate.rs deleted file mode 100644 index a34bc640a..000000000 --- a/crates/mun_compiler/src/annotate.rs +++ /dev/null @@ -1,216 +0,0 @@ -//! This module provides builders for integrating the [`annotate-snippets`] crate with Mun. -//! -//! [`annotate-snippets`]: https://docs.rs/annotate-snippets/0.6.1/annotate_snippets/ - -use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}; -use mun_hir::line_index::LineIndex; - -use unicode_segmentation::UnicodeSegmentation; - -pub struct SnippetBuilder { - snippet: Snippet, -} - -impl Default for SnippetBuilder { - fn default() -> Self { - SnippetBuilder { - snippet: Snippet { - title: None, - footer: vec![], - slices: vec![], - }, - } - } -} - -impl SnippetBuilder { - pub fn new() -> SnippetBuilder { - SnippetBuilder::default() - } - pub fn title(mut self, title: Annotation) -> SnippetBuilder { - self.snippet.title = Some(title); - self - } - pub fn footer(mut self, footer: Annotation) -> SnippetBuilder { - self.snippet.footer.push(footer); - self - } - pub fn slice(mut self, slice: Slice) -> SnippetBuilder { - self.snippet.slices.push(slice); - self - } - pub fn build(self) -> Snippet { - self.snippet - } -} - -pub struct SliceBuilder { - slice: Slice, -} - -impl SliceBuilder { - pub fn new(fold: bool) -> SliceBuilder { - SliceBuilder { - slice: Slice { - source: String::new(), - line_start: 0, - origin: None, - annotations: Vec::new(), - fold, - }, - } - } - - pub fn origin(mut self, relative_file_path: &str) -> SliceBuilder { - self.slice.origin = Some(relative_file_path.to_string()); - self - } - - pub fn source_annotation( - mut self, - range: (usize, usize), - label: &str, - source_annotation_type: AnnotationType, - ) -> SliceBuilder { - self.slice.annotations.push(SourceAnnotation { - range, - label: label.to_string(), - annotation_type: source_annotation_type, - }); - self - } - - pub fn build(mut self, source_text: &str, line_index: &LineIndex) -> Slice { - // Variable for storing the first and last line of the used source code - let mut fl_lines: Option<(u32, u32)> = None; - - // Find the range of lines that include all highlighted segments - for annotation in &self.slice.annotations { - if let Some(range) = fl_lines { - fl_lines = Some(( - line_index - .line_col(range.0.into()) - .line - .min(line_index.line_col((annotation.range.0 as u32).into()).line), - line_index - .line_col(range.1.into()) - .line - .max(line_index.line_col((annotation.range.1 as u32).into()).line), - )); - } else { - fl_lines = Some(( - line_index.line_col((annotation.range.0 as u32).into()).line, - line_index.line_col((annotation.range.1 as u32).into()).line, - )); - } - } - - if let Some(fl_lines) = fl_lines { - self.slice.line_start = fl_lines.0 as usize + 1; - let first_line_offset = line_index.line_offset(fl_lines.0); - - // Extract the required range of lines - self.slice.source = line_index - .text_part(fl_lines.0, fl_lines.1, source_text, source_text.len()) - .unwrap() - .to_string(); - - // Convert annotation ranges based on the cropped region, indexable by unicode - // graphemes (required for aligned annotations) - let convertor_function = |source: &String, annotation_range_border: usize| { - UnicodeSegmentation::graphemes( - &source[0..(annotation_range_border - first_line_offset)], - true).count() - // this addend is a fix for annotate-snippets issue number 24 - + (line_index.line_col((annotation_range_border as u32).into()).line - - fl_lines.0) as usize - }; - for annotation in self.slice.annotations.iter_mut() { - annotation.range = ( - convertor_function(&self.slice.source, annotation.range.0), - convertor_function(&self.slice.source, annotation.range.1), - ); - } - } - self.slice - } -} - -pub struct AnnotationBuilder { - annotation: Annotation, -} - -impl AnnotationBuilder { - pub fn new(annotation_type: AnnotationType) -> AnnotationBuilder { - AnnotationBuilder { - annotation: Annotation { - id: None, - label: None, - annotation_type, - }, - } - } - - pub fn id(mut self, id: &str) -> AnnotationBuilder { - self.annotation.id = Some(id.to_string()); - self - } - - pub fn label(mut self, label: &str) -> AnnotationBuilder { - self.annotation.label = Some(label.to_string()); - self - } - - pub fn build(self) -> Annotation { - self.annotation - } -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn annotation_builder_snapshot() { - insta::assert_debug_snapshot!(AnnotationBuilder::new(AnnotationType::Note) - .id("1") - .label("test annotation") - .build()); - } - #[test] - fn slice_builder_snapshot() { - let source_code = "fn foo()->f64{\n48\n}"; - let line_index: LineIndex = LineIndex::new(source_code); - - insta::assert_debug_snapshot!(SliceBuilder::new(true) - .origin("/tmp/usr/test.mun") - .source_annotation((13, 19), "test source annotation", AnnotationType::Note) - .build(source_code, &line_index)); - } - #[test] - fn snippet_builder_snapshot() { - let source_code = "fn foo()->f64{\n48\n}\n\nfn bar()->bool{\n23\n}"; - let line_index: LineIndex = LineIndex::new(source_code); - - insta::assert_debug_snapshot!(SnippetBuilder::new() - .title( - AnnotationBuilder::new(AnnotationType::Note) - .id("1") - .label("test annotation") - .build() - ) - .footer( - AnnotationBuilder::new(AnnotationType::Warning) - .id("2") - .label("test annotation") - .build() - ) - .slice( - SliceBuilder::new(true) - .origin("/tmp/usr/test.mun") - .source_annotation((14, 20), "test source annotation", AnnotationType::Note,) - .source_annotation((35, 41), "test source annotation", AnnotationType::Error,) - .build(source_code, &line_index) - ) - .build()); - } -} diff --git a/crates/mun_compiler/src/diagnostics.rs b/crates/mun_compiler/src/diagnostics.rs index f81abbad0..622f764cc 100644 --- a/crates/mun_compiler/src/diagnostics.rs +++ b/crates/mun_compiler/src/diagnostics.rs @@ -1,158 +1,3 @@ -use mun_hir::diagnostics::DiagnosticSink; -use mun_hir::{FileId, HirDatabase, Module}; - -use std::cell::RefCell; - -use crate::diagnostics_snippets; -use annotate_snippets::{ - display_list::DisplayList, formatter::DisplayListFormatter, snippet::Snippet, -}; - -/// Emits all specified diagnostic messages to the given stream -pub fn emit_diagnostics<'a>( - writer: &mut dyn std::io::Write, - diagnostics: impl IntoIterator, - colors: bool, -) -> Result<(), anyhow::Error> { - let dlf = DisplayListFormatter::new(colors, false); - for diagnostic in diagnostics.into_iter() { - let dl = DisplayList::from(diagnostic.clone()); - writeln!(writer, "{}", dlf.format(&dl))?; - } - Ok(()) -} - -/// Constructs diagnostic messages for the given file. -pub fn diagnostics(db: &dyn HirDatabase, file_id: FileId) -> Vec { - let parse = db.parse(file_id); - - let mut result = Vec::new(); - // Replace every `\t` symbol by one whitespace in source code because in console it is - // displaying like 1-4 spaces(depending on it position) and by this it breaks highlighting. - // In future here, instead of `replace("\t", " ")`, can be implemented algorithm that - // correctly replace each `\t` into 1-4 space. - let source_code = db.file_text(file_id).to_string().replace("\t", " "); - - let relative_file_path = db.file_relative_path(file_id).to_string(); - - let line_index = db.line_index(file_id); - - result.extend(parse.errors().iter().map(|err| { - diagnostics_snippets::syntax_error( - err, - db, - &parse, - &relative_file_path, - &source_code, - &line_index, - ) - })); - - let result = RefCell::new(result); - let mut sink = DiagnosticSink::new(|d| { - result - .borrow_mut() - .push(diagnostics_snippets::generic_error( - d, - db, - &parse, - &relative_file_path, - &source_code, - &line_index, - )); - }) - .on::(|d| { - result - .borrow_mut() - .push(diagnostics_snippets::unresolved_value_error( - d, - db, - &parse, - &relative_file_path, - &source_code, - &line_index, - )); - }) - .on::(|d| { - result - .borrow_mut() - .push(diagnostics_snippets::unresolved_type_error( - d, - db, - &parse, - &relative_file_path, - &source_code, - &line_index, - )); - }) - .on::(|d| { - result - .borrow_mut() - .push(diagnostics_snippets::expected_function_error( - d, - db, - &parse, - &relative_file_path, - &source_code, - &line_index, - )); - }) - .on::(|d| { - result - .borrow_mut() - .push(diagnostics_snippets::mismatched_type_error( - d, - db, - &parse, - &relative_file_path, - &source_code, - &line_index, - )); - }) - .on::(|d| { - result - .borrow_mut() - .push(diagnostics_snippets::duplicate_definition_error( - d, - db, - &parse, - &relative_file_path, - &source_code, - &line_index, - )); - }) - .on::(|d| { - result - .borrow_mut() - .push(diagnostics_snippets::possibly_uninitialized_variable_error( - d, - db, - &parse, - &relative_file_path, - &source_code, - &line_index, - )); - }) - .on::(|d| { - result - .borrow_mut() - .push(diagnostics_snippets::access_unknown_field_error( - d, - db, - &parse, - &relative_file_path, - &source_code, - &line_index, - )); - }); - - Module::from(file_id).diagnostics(db, &mut sink); - - drop(sink); - - result.into_inner() -} - #[cfg(test)] mod tests { use crate::{Config, DisplayColor, Driver, PathOrInline, RelativePathBuf}; diff --git a/crates/mun_compiler/src/diagnostics_snippets.rs b/crates/mun_compiler/src/diagnostics_snippets.rs index 0f2ff31e9..323e1d747 100644 --- a/crates/mun_compiler/src/diagnostics_snippets.rs +++ b/crates/mun_compiler/src/diagnostics_snippets.rs @@ -1,363 +1,200 @@ -use mun_hir::diagnostics::Diagnostic as HirDiagnostic; -use mun_hir::{HirDatabase, HirDisplay}; -use mun_syntax::{ - ast, AstNode, Parse, SourceFile, SyntaxError, SyntaxKind, SyntaxNodePtr, TextRange, -}; +use mun_diagnostics::DiagnosticForWith; +use mun_hir::{FileId, HirDatabase, RelativePathBuf}; +use mun_syntax::SyntaxError; use std::sync::Arc; use mun_hir::line_index::LineIndex; -use crate::annotate::{AnnotationBuilder, SliceBuilder, SnippetBuilder}; +use annotate_snippets::display_list::DisplayList; +use annotate_snippets::display_list::FormatOptions; +use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}; +use std::collections::HashMap; -use annotate_snippets::snippet::{AnnotationType, Snippet}; - -fn text_range_to_tuple(text_range: TextRange) -> (usize, usize) { - (text_range.start().to_usize(), text_range.end().to_usize()) -} - -fn syntax_node_ptr_location( - syntax_node_ptr: SyntaxNodePtr, - parse: &Parse, -) -> TextRange { - match syntax_node_ptr.kind() { - SyntaxKind::FUNCTION_DEF => { - ast::FunctionDef::cast(syntax_node_ptr.to_node(parse.tree().syntax())) - .map(|f| f.signature_range()) - .unwrap_or_else(|| syntax_node_ptr.range()) - } - SyntaxKind::STRUCT_DEF => { - ast::StructDef::cast(syntax_node_ptr.to_node(parse.tree().syntax())) - .map(|s| s.signature_range()) - .unwrap_or_else(|| syntax_node_ptr.range()) - } - _ => syntax_node_ptr.range(), - } -} - -pub(crate) fn syntax_error( +pub(crate) fn emit_syntax_error( syntax_error: &SyntaxError, - _: &dyn HirDatabase, - _: &Parse, relative_file_path: &str, source_code: &str, - line_index: &Arc, -) -> Snippet { - let mut snippet = SnippetBuilder::new() - .title( - AnnotationBuilder::new(AnnotationType::Error) - .label("syntax error") - .build(), - ) - .slice( - SliceBuilder::new(true) - .origin(relative_file_path) - .source_annotation( - ( - syntax_error.location().offset().to_usize(), - syntax_error.location().end_offset().to_usize(), - ), - &syntax_error.to_string(), - AnnotationType::Error, - ) - .build(&source_code, &line_index), - ) - .build(); - // Add one to right range to make highlighting range here visible on output - snippet.slices[0].annotations[0].range.1 += 1; - - snippet + line_index: &LineIndex, + display_colors: bool, + writer: &mut dyn std::io::Write, +) -> std::io::Result<()> { + let syntax_error_text = syntax_error.to_string(); + let location = syntax_error.location(); + let line = line_index.line_col(location.offset()).line; + let line_offset = line_index.line_offset(line); + + let snippet = Snippet { + title: Some(Annotation { + id: None, + label: Some("syntax error"), + annotation_type: AnnotationType::Error, + }), + footer: vec![], + slices: vec![Slice { + source: &source_code[line_offset..], + line_start: line as usize + 1, + origin: Some(relative_file_path), + annotations: vec![SourceAnnotation { + range: ( + location.offset().to_usize() - line_offset, + location.end_offset().to_usize() - line_offset + 1, + ), + label: &syntax_error_text, + annotation_type: AnnotationType::Error, + }], + fold: true, + }], + opt: FormatOptions { + color: display_colors, + anonymized_line_numbers: false, + margin: None, + }, + }; + let dl = DisplayList::from(snippet); + write!(writer, "{}", dl) } -pub(crate) fn generic_error( - diagnostic: &dyn HirDiagnostic, - _: &dyn HirDatabase, - _: &Parse, - relative_file_path: &str, - source_code: &str, - line_index: &Arc, -) -> Snippet { - SnippetBuilder::new() - .title( - AnnotationBuilder::new(AnnotationType::Error) - .label(&diagnostic.message()) - .build(), - ) - .slice( - SliceBuilder::new(true) - .origin(relative_file_path) - .source_annotation( - text_range_to_tuple(diagnostic.highlight_range()), - &diagnostic.message(), - AnnotationType::Error, - ) - .build(&source_code, &line_index), - ) - .build() +/// Emits all diagnostics that are a result of HIR validation. +pub(crate) fn emit_hir_diagnostic( + diagnostic: &dyn mun_hir::Diagnostic, + db: &impl HirDatabase, + file_id: FileId, + display_colors: bool, + writer: &mut dyn std::io::Write, +) -> std::io::Result<()> { + diagnostic.with_diagnostic(db, |diagnostic| { + emit_diagnostic(diagnostic, db, file_id, display_colors, writer) + }) } -pub(crate) fn unresolved_value_error( - diagnostic: &mun_hir::diagnostics::UnresolvedValue, - _: &dyn HirDatabase, - parse: &Parse, - relative_file_path: &str, - source_code: &str, - line_index: &Arc, -) -> Snippet { - let unresolved_value = diagnostic - .expr - .to_node(&parse.tree().syntax()) - .text() - .to_string(); - - SnippetBuilder::new() - .title( - AnnotationBuilder::new(AnnotationType::Error) - .label(&format!( - "cannot find value `{}` in this scope", - unresolved_value - )) - .build(), - ) - .slice( - SliceBuilder::new(true) - .origin(relative_file_path) - .source_annotation( - text_range_to_tuple(diagnostic.highlight_range()), - "not found in this scope", - AnnotationType::Error, - ) - .build(&source_code, &line_index), - ) - .build() -} - -pub(crate) fn unresolved_type_error( - diagnostic: &mun_hir::diagnostics::UnresolvedType, - _: &dyn HirDatabase, - parse: &Parse, - relative_file_path: &str, - source_code: &str, - line_index: &Arc, -) -> Snippet { - let unresolved_type = diagnostic - .type_ref - .to_node(&parse.syntax_node()) - .syntax() - .text() - .to_string(); - - SnippetBuilder::new() - .title( - AnnotationBuilder::new(AnnotationType::Error) - .label(&format!( - "cannot find type `{}` in this scope", - unresolved_type - )) - .build(), - ) - .slice( - SliceBuilder::new(true) - .origin(relative_file_path) - .source_annotation( - text_range_to_tuple(diagnostic.highlight_range()), - "not found in this scope", - AnnotationType::Error, - ) - .build(&source_code, &line_index), - ) - .build() -} - -pub(crate) fn expected_function_error( - diagnostic: &mun_hir::diagnostics::ExpectedFunction, - hir_database: &dyn HirDatabase, - _: &Parse, - relative_file_path: &str, - source_code: &str, - line_index: &Arc, -) -> Snippet { - SnippetBuilder::new() - .title( - AnnotationBuilder::new(AnnotationType::Error) - .label(&diagnostic.message()) - .build(), - ) - .slice( - SliceBuilder::new(true) - .origin(relative_file_path) - .source_annotation( - text_range_to_tuple(diagnostic.highlight_range()), - &format!( - "expected function, found `{}`", - diagnostic.found.display(hir_database) - ), - AnnotationType::Error, - ) - .build(&source_code, &line_index), - ) - .build() -} - -pub(crate) fn mismatched_type_error( - diagnostic: &mun_hir::diagnostics::MismatchedType, - hir_database: &dyn HirDatabase, - _: &Parse, - relative_file_path: &str, - source_code: &str, - line_index: &Arc, -) -> Snippet { - SnippetBuilder::new() - .title( - AnnotationBuilder::new(AnnotationType::Error) - .label(&diagnostic.message()) - .build(), - ) - .slice( - SliceBuilder::new(true) - .origin(relative_file_path) - .source_annotation( - text_range_to_tuple(diagnostic.highlight_range()), - &format!( - "expected `{}`, found `{}`", - diagnostic.expected.display(hir_database), - diagnostic.found.display(hir_database) - ), - AnnotationType::Error, - ) - .build(&source_code, &line_index), - ) - .build() -} - -pub(crate) fn duplicate_definition_error( - diagnostic: &mun_hir::diagnostics::DuplicateDefinition, - _: &dyn HirDatabase, - parse: &Parse, - relative_file_path: &str, - source_code: &str, - line_index: &Arc, -) -> Snippet { - let first_definition_location = syntax_node_ptr_location(diagnostic.first_definition, &parse); - let definition_location = syntax_node_ptr_location(diagnostic.definition, &parse); - - let duplication_object_type = - if matches!(diagnostic.first_definition.kind(), SyntaxKind::STRUCT_DEF) - && matches!(diagnostic.definition.kind(), SyntaxKind::STRUCT_DEF) - { - "type" - } else { - "value" - }; - - SnippetBuilder::new() - .title( - AnnotationBuilder::new(AnnotationType::Error) - .label(&diagnostic.message()) - .build(), - ) - .slice( - SliceBuilder::new(true) - .origin(relative_file_path) - // First definition - .source_annotation( - text_range_to_tuple(first_definition_location), - &format!( - "previous definition of the {} `{}` here", - duplication_object_type, diagnostic.name - ), - AnnotationType::Warning, - ) - // Second definition - .source_annotation( - text_range_to_tuple(definition_location), - &format!("`{}` redefined here", diagnostic.name), - AnnotationType::Error, - ) - .build(&source_code, &line_index), - ) - .footer( - AnnotationBuilder::new(AnnotationType::Note) - .label(&format!( - "`{}` must be defined only once in the {} namespace of this module", - diagnostic.name, duplication_object_type - )) - .build(), - ) - .build() -} - -pub(crate) fn possibly_uninitialized_variable_error( - diagnostic: &mun_hir::diagnostics::PossiblyUninitializedVariable, - _: &dyn HirDatabase, - parse: &Parse, - relative_file_path: &str, - source_code: &str, - line_index: &Arc, -) -> Snippet { - let variable_name = diagnostic.pat.to_node(&parse.syntax_node()).text(); - - SnippetBuilder::new() - .title( - AnnotationBuilder::new(AnnotationType::Error) - .label(&format!("{}: `{}`", diagnostic.message(), variable_name)) - .build(), - ) - .slice( - SliceBuilder::new(true) - .origin(relative_file_path) - .source_annotation( - text_range_to_tuple(diagnostic.highlight_range()), - &format!("use of possibly-uninitialized `{}`", variable_name), - AnnotationType::Error, - ) - .build(&source_code, &line_index), - ) - .build() -} - -pub(crate) fn access_unknown_field_error( - diagnostic: &mun_hir::diagnostics::AccessUnknownField, - hir_database: &dyn HirDatabase, - parse: &Parse, - relative_file_path: &str, - source_code: &str, - line_index: &Arc, -) -> Snippet { - let location = ast::FieldExpr::cast(diagnostic.expr.to_node(&parse.syntax_node())) - .map(|f| f.field_range()) - .unwrap_or_else(|| diagnostic.highlight_range()); - - SnippetBuilder::new() - .title( - AnnotationBuilder::new(AnnotationType::Error) - .label(&format!( - "no field `{}` on type `{}`", - diagnostic.name, - diagnostic.receiver_ty.display(hir_database), - )) - .build(), - ) - .slice( - SliceBuilder::new(true) - .origin(relative_file_path) - .source_annotation( - text_range_to_tuple(location), - "unknown field", - AnnotationType::Error, - ) - .build(&source_code, &line_index), - ) - .build() -} - -#[cfg(test)] -mod tests { - use super::*; +/// Emits a diagnostic by writting a snippet to the specified `writer`. +fn emit_diagnostic( + diagnostic: &dyn mun_diagnostics::Diagnostic, + db: &impl HirDatabase, + file_id: FileId, + display_colors: bool, + writer: &mut dyn std::io::Write, +) -> std::io::Result<()> { + // Get the basic info from the diagnostic + let title = diagnostic.title(); + let range = diagnostic.range(); + + /// Will hold all snippets and their relevant information + struct AnnotationFile { + relative_file_path: RelativePathBuf, + source_code: Arc, + line_index: Arc, + annotations: Vec, + }; + + let annotations = { + let mut annotations = Vec::new(); + let mut file_to_index = HashMap::new(); + + // Add primary annotations + annotations.push(AnnotationFile { + relative_file_path: db.file_relative_path(file_id), + source_code: db.file_text(file_id), + line_index: db.line_index(file_id), + annotations: vec![match diagnostic.primary_annotation() { + None => mun_diagnostics::SourceAnnotation { + range, + message: title.clone(), + }, + Some(annotation) => annotation, + }], + }); + file_to_index.insert(file_id, 0); + + // Add the secondary annotations + for annotation in diagnostic.secondary_annotations() { + let file_id = annotation.range.file_id; + + // Find an entry for this `file_id` + let file_idx = match file_to_index.get(&file_id) { + None => { + // Doesn't exist yet, add it + annotations.push(AnnotationFile { + relative_file_path: db.file_relative_path(file_id), + source_code: db.file_text(file_id), + line_index: db.line_index(file_id), + annotations: Vec::new(), + }); + let idx = annotations.len() - 1; + file_to_index.insert(file_id, idx); + idx + } + Some(idx) => *idx, + }; + + // Add this annotation to the list of snippets for the file + annotations[file_idx].annotations.push(annotation.into()); + } - #[test] - fn test_text_range_to_tuple() { - let text_range = TextRange::from_to(3.into(), 5.into()); - assert_eq!(text_range_to_tuple(text_range), (3, 5)); - } + annotations + }; + + let footer = diagnostic.footer(); + + // Construct an annotation snippet to be able to emit it. + let snippet = Snippet { + title: Some(Annotation { + id: None, + label: Some(&title), + annotation_type: AnnotationType::Error, + }), + slices: annotations + .iter() + .filter_map(|file| { + let first_offset = { + let mut iter = file.annotations.iter(); + match iter.next() { + Some(first) => { + let first = first.range.start(); + iter.fold(first, |init, value| init.min(value.range.start())) + } + None => return None, + } + }; + let first_offset_line = file.line_index.line_col(first_offset); + let line_offset = file.line_index.line_offset(first_offset_line.line); + Some(Slice { + source: &file.source_code[line_offset..], + line_start: first_offset_line.line as usize + 1, + origin: Some(file.relative_file_path.as_ref()), + annotations: file + .annotations + .iter() + .map(|annotation| SourceAnnotation { + range: ( + annotation.range.start().to_usize() - line_offset, + annotation.range.end().to_usize() - line_offset, + ), + label: annotation.message.as_str(), + annotation_type: AnnotationType::Error, + }) + .collect(), + fold: true, + }) + }) + .collect(), + footer: footer + .iter() + .map(|footer| Annotation { + id: None, + label: Some(footer.as_str()), + annotation_type: AnnotationType::Note, + }) + .collect(), + opt: FormatOptions { + color: display_colors, + anonymized_line_numbers: false, + margin: None, + }, + }; + + // Build a display list and emit to the writer + let dl = DisplayList::from(snippet); + write!(writer, "{}", dl) } diff --git a/crates/mun_compiler/src/driver.rs b/crates/mun_compiler/src/driver.rs index cdbadac26..b5db96a57 100644 --- a/crates/mun_compiler/src/driver.rs +++ b/crates/mun_compiler/src/driver.rs @@ -2,13 +2,11 @@ //! from previous compilation. use crate::{ - compute_source_relative_path, - db::CompilerDatabase, - diagnostics::{diagnostics, emit_diagnostics}, - ensure_package_output_dir, is_source_file, PathOrInline, RelativePath, + compute_source_relative_path, db::CompilerDatabase, ensure_package_output_dir, is_source_file, + PathOrInline, RelativePath, }; use mun_codegen::{Assembly, IrDatabase}; -use mun_hir::{FileId, RelativePathBuf, SourceDatabase, SourceRoot, SourceRootId}; +use mun_hir::{DiagnosticSink, FileId, RelativePathBuf, SourceDatabase, SourceRoot, SourceRootId}; use std::{path::PathBuf, sync::Arc}; @@ -18,7 +16,7 @@ mod display_color; pub use self::config::Config; pub use self::display_color::DisplayColor; -use annotate_snippets::snippet::{AnnotationType, Snippet}; +use crate::diagnostics_snippets::{emit_hir_diagnostic, emit_syntax_error}; use mun_project::Package; use std::collections::HashMap; use std::convert::TryInto; @@ -192,34 +190,50 @@ impl Driver { } impl Driver { - /// Returns a vector containing all the diagnostic messages for the project. - pub fn diagnostics(&self) -> Vec { - self.db - .source_root(WORKSPACE) - .files() - .map(|f| diagnostics(&self.db, f)) - .flatten() - .collect() - } - /// Emits all diagnostic messages currently in the database; returns true if errors were /// emitted. pub fn emit_diagnostics(&self, writer: &mut dyn std::io::Write) -> Result { - let diagnostics = self.diagnostics(); - - // Emit all diagnostics to the stream - emit_diagnostics(writer, &diagnostics, self.display_color.should_enable())?; - - // Determine if one of the snippets is actually an error - Ok(diagnostics.iter().any(|d| { - d.title - .as_ref() - .map(|a| match a.annotation_type { - AnnotationType::Error => true, - _ => false, - }) - .unwrap_or(false) - })) + // Iterate over all files in the workspace + let emit_colors = self.display_color.should_enable(); + let mut has_error = false; + for file_id in self.db.source_root(WORKSPACE).files() { + let parse = self.db.parse(file_id); + let source_code = self.db.file_text(file_id); + let relative_file_path = self.db.file_relative_path(file_id); + let line_index = self.db.line_index(file_id); + + // Emit all syntax diagnostics + for syntax_error in parse.errors().iter() { + emit_syntax_error( + syntax_error, + relative_file_path.as_str(), + source_code.as_str(), + &line_index, + emit_colors, + writer, + )?; + has_error = true; + } + + // Emit all HIR diagnostics + let mut error = None; + mun_hir::Module::from(file_id).diagnostics( + &self.db, + &mut DiagnosticSink::new(|d| { + has_error = true; + if let Err(e) = emit_hir_diagnostic(d, &self.db, file_id, emit_colors, writer) { + error = Some(e) + }; + }), + ); + + // If an error occurred when emitting HIR diagnostics, return early with the error. + if let Some(e) = error { + return Err(e.into()); + } + } + + Ok(has_error) } } diff --git a/crates/mun_compiler/src/lib.rs b/crates/mun_compiler/src/lib.rs index 530c3abde..d3aadc021 100644 --- a/crates/mun_compiler/src/lib.rs +++ b/crates/mun_compiler/src/lib.rs @@ -1,5 +1,5 @@ #![allow(clippy::enum_variant_names)] // This is a HACK because we use salsa -mod annotate; + //mod annotate; mod db; ///! This library contains the code required to go from source code to binaries. pub mod diagnostics; @@ -12,7 +12,6 @@ use std::path::{Path, PathBuf}; pub use crate::driver::DisplayColor; pub use crate::driver::{Config, Driver}; -pub use annotate::{AnnotationBuilder, SliceBuilder, SnippetBuilder}; pub use mun_codegen::OptimizationLevel; pub use crate::db::CompilerDatabase; diff --git a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__access_unknown_field_error.snap b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__access_unknown_field_error.snap index 92aa1969f..69ac65c1b 100644 --- a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__access_unknown_field_error.snap +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__access_unknown_field_error.snap @@ -3,9 +3,8 @@ source: crates/mun_compiler/src/diagnostics.rs expression: "compilation_errors(\"\\n\\nstruct Foo {\\ni: bool\\n}\\n\\nfn main() {\\nlet a = Foo { i: false };\\nlet b = a.t;\\n}\")" --- error: no field `t` on type `Foo` - --> main.mun:9:10 + --> main.mun:9:11 | 9 | let b = a.t; | ^ unknown field | - diff --git a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__duplicate_definition_error.snap b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__duplicate_definition_error.snap index cfe4538b6..df0edda7b 100644 --- a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__duplicate_definition_error.snap +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__duplicate_definition_error.snap @@ -2,41 +2,31 @@ source: crates/mun_compiler/src/diagnostics.rs expression: "compilation_errors(\"\\n\\nfn foo(){}\\n\\nfn foo(){}\\n\\nstruct Bar;\\n\\nstruct Bar;\\n\\nfn BAZ(){}\\n\\nstruct BAZ;\")" --- -error: the name `foo` is defined multiple times - --> main.mun:3:0 +error: a value named `foo` has already been defined in this module + --> main.mun:5:1 | 3 | fn foo(){} - | -------- previous definition of the value `foo` here + | ^^^^^^^^ first definition of the value `foo` here 4 | 5 | fn foo(){} | ^^^^^^^^ `foo` redefined here | - = note: `foo` must be defined only once in the value namespace of this module -error: the name `Bar` is defined multiple times - --> main.mun:3:0 + = note: `foo` must be defined only once in the value namespace of this moduleerror: a type named `Bar` has already been defined in this module + --> main.mun:9:1 | -... 7 | struct Bar; - | ---------- previous definition of the type `Bar` here + | ^^^^^^^^^^ first definition of the type `Bar` here 8 | 9 | struct Bar; | ^^^^^^^^^^ `Bar` redefined here | - = note: `Bar` must be defined only once in the type namespace of this module -error: the name `BAZ` is defined multiple times - --> main.mun:8:0 + = note: `Bar` must be defined only once in the type namespace of this moduleerror: a type named `BAZ` has already been defined in this module + --> main.mun:13:1 | - 3 | fn foo(){} - 4 | - 5 | fn foo(){} - 6 | -... -10 | 11 | fn BAZ(){} - | -------- previous definition of the value `BAZ` here + | ^^^^^^^^ first definition of the type `BAZ` here 12 | 13 | struct BAZ; | ^^^^^^^^^^ `BAZ` redefined here | - = note: `BAZ` must be defined only once in the value namespace of this module - + = note: `BAZ` must be defined only once in the type namespace of this module diff --git a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__expected_function_error.snap b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__expected_function_error.snap index c5601c14e..c3c46346e 100644 --- a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__expected_function_error.snap +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__expected_function_error.snap @@ -3,27 +3,23 @@ source: crates/mun_compiler/src/diagnostics.rs expression: "compilation_errors(\"\\n\\nfn main() {\\nlet a = Foo();\\n\\nlet b = Bar();\\n}\")" --- error: cannot find value `Foo` in this scope - --> main.mun:4:8 + --> main.mun:4:9 | 4 | let a = Foo(); | ^^^ not found in this scope - | -error: expected function type - --> main.mun:4:8 + |error: expected function, found `{unknown}` + --> main.mun:4:9 | 4 | let a = Foo(); - | ^^^ expected function, found `{unknown}` - | -error: cannot find value `Bar` in this scope - --> main.mun:6:8 + | ^^^ not a function + |error: cannot find value `Bar` in this scope + --> main.mun:6:9 | 6 | let b = Bar(); | ^^^ not found in this scope - | -error: expected function type - --> main.mun:6:8 + |error: expected function, found `{unknown}` + --> main.mun:6:9 | 6 | let b = Bar(); - | ^^^ expected function, found `{unknown}` + | ^^^ not a function | - diff --git a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__mismatched_type_error.snap b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__mismatched_type_error.snap index 278626afe..946aa80c0 100644 --- a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__mismatched_type_error.snap +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__mismatched_type_error.snap @@ -2,16 +2,14 @@ source: crates/mun_compiler/src/diagnostics.rs expression: "compilation_errors(\"\\n\\nfn main() {\\nlet a: f64 = false;\\n\\nlet b: bool = 22;\\n}\")" --- -error: mismatched type - --> main.mun:4:13 +error: expected `f64`, found `bool` + --> main.mun:4:14 | 4 | let a: f64 = false; | ^^^^^ expected `f64`, found `bool` - | -error: mismatched type - --> main.mun:6:14 + |error: expected `bool`, found `{integer}` + --> main.mun:6:15 | 6 | let b: bool = 22; | ^^ expected `bool`, found `{integer}` | - diff --git a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__possibly_uninitialized_variable_error.snap b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__possibly_uninitialized_variable_error.snap index de09cfe41..422d65552 100644 --- a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__possibly_uninitialized_variable_error.snap +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__possibly_uninitialized_variable_error.snap @@ -2,10 +2,9 @@ source: crates/mun_compiler/src/diagnostics.rs expression: "compilation_errors(\"\\n\\nfn main() {\\nlet a;\\nif 5>6 {\\na = 5\\n}\\nlet b = a;\\n}\")" --- -error: use of possibly-uninitialized variable: `a` - --> main.mun:8:8 +error: use of possibly-uninitialized `a` + --> main.mun:8:9 | 8 | let b = a; | ^ use of possibly-uninitialized `a` | - diff --git a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__syntax_error.snap b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__syntax_error.snap index 2f2fc5bc1..10d97f66e 100644 --- a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__syntax_error.snap +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__syntax_error.snap @@ -3,27 +3,23 @@ source: crates/mun_compiler/src/diagnostics.rs expression: "compilation_errors(\"\\n\\nfn main(\\n struct Foo\\n\")" --- error: syntax error - --> main.mun:3:8 + --> main.mun:3:9 | 3 | fn main( | ^ expected value parameter - | -error: syntax error - --> main.mun:3:8 + |error: syntax error + --> main.mun:3:9 | 3 | fn main( | ^ expected R_PAREN - | -error: syntax error - --> main.mun:3:8 + |error: syntax error + --> main.mun:3:9 | 3 | fn main( | ^ expected a block - | -error: syntax error - --> main.mun:4:11 + |error: syntax error + --> main.mun:4:12 | 4 | struct Foo | ^ expected a ';', '{', or '(' | - diff --git a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__unresolved_type_error.snap b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__unresolved_type_error.snap index faadb682f..dd8bb198f 100644 --- a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__unresolved_type_error.snap +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__unresolved_type_error.snap @@ -3,15 +3,13 @@ source: crates/mun_compiler/src/diagnostics.rs expression: "compilation_errors(\"\\n\\nfn main() {\\nlet a = Foo{};\\n\\nlet b = Bar{};\\n}\")" --- error: cannot find type `Foo` in this scope - --> main.mun:4:8 + --> main.mun:4:9 | 4 | let a = Foo{}; | ^^^ not found in this scope - | -error: cannot find type `Bar` in this scope - --> main.mun:6:8 + |error: cannot find type `Bar` in this scope + --> main.mun:6:9 | 6 | let b = Bar{}; | ^^^ not found in this scope | - diff --git a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__unresolved_value_error.snap b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__unresolved_value_error.snap index 3d92526e5..dc7dc6fc5 100644 --- a/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__unresolved_value_error.snap +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__unresolved_value_error.snap @@ -3,15 +3,13 @@ source: crates/mun_compiler/src/diagnostics.rs expression: "compilation_errors(\"\\n\\nfn main() {\\nlet b = a;\\n\\nlet d = c;\\n}\")" --- error: cannot find value `a` in this scope - --> main.mun:4:8 + --> main.mun:4:9 | 4 | let b = a; | ^ not found in this scope - | -error: cannot find value `c` in this scope - --> main.mun:6:8 + |error: cannot find value `c` in this scope + --> main.mun:6:9 | 6 | let d = c; | ^ not found in this scope | - diff --git a/crates/mun_diagnostics/Cargo.toml b/crates/mun_diagnostics/Cargo.toml index 851c667d1..54e6dd43e 100644 --- a/crates/mun_diagnostics/Cargo.toml +++ b/crates/mun_diagnostics/Cargo.toml @@ -15,5 +15,5 @@ categories = ["game-development", "mun"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -hir = { version = "=0.2.0", path="../mun_hir", package="mun_hir" } +mun_hir = { version = "=0.2.0", path="../mun_hir", package="mun_hir" } mun_syntax = { version = "=0.2.0", path = "../mun_syntax" } diff --git a/crates/mun_diagnostics/src/hir.rs b/crates/mun_diagnostics/src/hir.rs index f2fe2b44b..644519e8b 100644 --- a/crates/mun_diagnostics/src/hir.rs +++ b/crates/mun_diagnostics/src/hir.rs @@ -1,4 +1,6 @@ +///! This module provides conversion from a `mun_hir::Diagnostics` to a `crate::Diagnostics`. mod access_unknown_field; +mod duplicate_definition_error; mod expected_function; mod mismatched_type; mod missing_fields; @@ -7,28 +9,32 @@ mod unresolved_type; mod unresolved_value; use crate::{Diagnostic, DiagnosticForWith, SourceAnnotation}; -use hir::Diagnostic as HirDiagnostic; +use mun_hir::Diagnostic as HirDiagnostic; use mun_syntax::TextRange; -// Provides conversion of a hir::Diagnostic to a crate::Diagnostic. This requires a database for +// Provides conversion of a mun_hir::Diagnostic to a crate::Diagnostic. This requires a database for // most operations. -impl DiagnosticForWith for dyn hir::Diagnostic { +impl DiagnosticForWith for dyn mun_hir::Diagnostic { fn with_diagnostic R>(&self, with: &DB, mut f: F) -> R { - if let Some(v) = self.downcast_ref::() { + if let Some(v) = self.downcast_ref::() { f(&unresolved_value::UnresolvedValue::new(with, v)) - } else if let Some(v) = self.downcast_ref::() { + } else if let Some(v) = self.downcast_ref::() { f(&unresolved_type::UnresolvedType::new(with, v)) - } else if let Some(v) = self.downcast_ref::() { + } else if let Some(v) = self.downcast_ref::() { f(&expected_function::ExpectedFunction::new(with, v)) - } else if let Some(v) = self.downcast_ref::() { + } else if let Some(v) = self.downcast_ref::() { f(&mismatched_type::MismatchedType::new(with, v)) } else if let Some(v) = - self.downcast_ref::() + self.downcast_ref::() { f(&possibly_unitialized_variable::PossiblyUninitializedVariable::new(with, v)) - } else if let Some(v) = self.downcast_ref::() { + } else if let Some(v) = self.downcast_ref::() { f(&access_unknown_field::AccessUnknownField::new(with, v)) - } else if let Some(v) = self.downcast_ref::() { + } else if let Some(v) = self.downcast_ref::() { + f(&duplicate_definition_error::DuplicateDefinition::new( + with, v, + )) + } else if let Some(v) = self.downcast_ref::() { f(&missing_fields::MissingFields::new(with, v)) } else { f(&GenericHirDiagnostic { diagnostic: self }) @@ -36,9 +42,9 @@ impl DiagnosticForWith for dyn hir::Diagnostic { } } -/// Diagnostic handler for generic hir diagnostics +/// Diagnostic handler for HIR diagnostics that do not have a specialized implementation. struct GenericHirDiagnostic<'diag> { - diagnostic: &'diag dyn hir::Diagnostic, + diagnostic: &'diag dyn mun_hir::Diagnostic, } impl<'diag> Diagnostic for GenericHirDiagnostic<'diag> { @@ -46,7 +52,7 @@ impl<'diag> Diagnostic for GenericHirDiagnostic<'diag> { self.diagnostic.highlight_range() } - fn label(&self) -> String { + fn title(&self) -> String { self.diagnostic.message() } diff --git a/crates/mun_diagnostics/src/hir/access_unknown_field.rs b/crates/mun_diagnostics/src/hir/access_unknown_field.rs index 5c770716b..439cf04cd 100644 --- a/crates/mun_diagnostics/src/hir/access_unknown_field.rs +++ b/crates/mun_diagnostics/src/hir/access_unknown_field.rs @@ -1,20 +1,32 @@ use super::HirDiagnostic; use crate::{Diagnostic, SourceAnnotation}; -use hir::HirDisplay; +use mun_hir::HirDisplay; use mun_syntax::{ast, AstNode, TextRange}; -pub struct AccessUnknownField<'db, 'diag, DB: hir::HirDatabase> { +/// An error that is emitted when trying to access a field that doesnt exist. +/// +/// ```mun +/// struct Foo { +/// b: i32 +/// } +/// +/// # fn main() { +/// let a = Foo { b: 3} +/// let b = a.c; // no field `c` +/// #} +/// ``` +pub struct AccessUnknownField<'db, 'diag, DB: mun_hir::HirDatabase> { db: &'db DB, - diag: &'diag hir::diagnostics::AccessUnknownField, + diag: &'diag mun_hir::diagnostics::AccessUnknownField, location: TextRange, } -impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic for AccessUnknownField<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for AccessUnknownField<'db, 'diag, DB> { fn range(&self) -> TextRange { self.location } - fn label(&self) -> String { + fn title(&self) -> String { format!( "no field `{}` on type `{}`", self.diag.name, @@ -30,9 +42,9 @@ impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic for AccessUnknownField<'db, 'd } } -impl<'db, 'diag, DB: hir::HirDatabase> AccessUnknownField<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> AccessUnknownField<'db, 'diag, DB> { /// Constructs a new instance of `AccessUnknownField` - pub fn new(db: &'db DB, diag: &'diag hir::diagnostics::AccessUnknownField) -> Self { + pub fn new(db: &'db DB, diag: &'diag mun_hir::diagnostics::AccessUnknownField) -> Self { let parse = db.parse(diag.file); let location = ast::FieldExpr::cast(diag.expr.to_node(&parse.syntax_node())) diff --git a/crates/mun_diagnostics/src/hir/duplicate_definition_error.rs b/crates/mun_diagnostics/src/hir/duplicate_definition_error.rs new file mode 100644 index 000000000..04ad2e448 --- /dev/null +++ b/crates/mun_diagnostics/src/hir/duplicate_definition_error.rs @@ -0,0 +1,154 @@ +use crate::{Diagnostic, SecondaryAnnotation, SourceAnnotation}; +use mun_hir::InFile; +use mun_syntax::{ast, AstNode, Parse, SourceFile, SyntaxKind, SyntaxNodePtr, TextRange}; + +/// For a given node returns the signature range (if that is applicable for the type of node) +/// ```rust, ignore +/// fn foo_bar() { +/// ^^^^^^^^^^^^___ this part +/// // ... +/// } +/// ``` +/// or +/// ```rust, ignore +/// pub(gc) struct Foo { +/// ^^^^^^^^^^___ this part +/// // ... +/// } +/// ``` +/// +/// If the specified syntax node is not a function definition or structure definition, returns the +/// range of the syntax node itself. +fn syntax_node_signature_range( + syntax_node_ptr: SyntaxNodePtr, + parse: &Parse, +) -> TextRange { + match syntax_node_ptr.kind() { + SyntaxKind::FUNCTION_DEF => { + ast::FunctionDef::cast(syntax_node_ptr.to_node(parse.tree().syntax())) + .map(|f| f.signature_range()) + .unwrap_or_else(|| syntax_node_ptr.range()) + } + SyntaxKind::STRUCT_DEF => { + ast::StructDef::cast(syntax_node_ptr.to_node(parse.tree().syntax())) + .map(|s| s.signature_range()) + .unwrap_or_else(|| syntax_node_ptr.range()) + } + _ => syntax_node_ptr.range(), + } +} + +/// For a given node returns the identifier range (if that is applicable for the type of node) +/// ```rust, ignore +/// fn foo_bar() { +/// ^^^^^^^___ this part +/// // ... +/// } +/// ``` +/// or +/// ```rust, ignore +/// pub(gc) struct Foo { +/// ^^^___ this part +/// // ... +/// } +/// ``` +/// +/// If the specified syntax node is not a function definition or structure definition, returns the +/// range of the syntax node itself. +fn syntax_node_identifier_range( + syntax_node_ptr: SyntaxNodePtr, + parse: &Parse, +) -> TextRange { + match syntax_node_ptr.kind() { + SyntaxKind::FUNCTION_DEF | SyntaxKind::STRUCT_DEF => syntax_node_ptr + .to_node(parse.tree().syntax()) + .children() + .find(|n| n.kind() == SyntaxKind::NAME) + .map(|name| name.text_range()) + .unwrap_or_else(|| syntax_node_ptr.range()), + _ => syntax_node_ptr.range(), + } +} + +/// An error that is emitted when a duplication definition is encountered: +/// +/// ```mun +/// struct Foo { +/// b: i32 +/// } +/// +/// struct Foo { // Duplicate definition +/// a: i32 +/// } +/// ``` +pub struct DuplicateDefinition<'db, 'diag, DB: mun_hir::HirDatabase> { + db: &'db DB, + diag: &'diag mun_hir::diagnostics::DuplicateDefinition, +} + +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for DuplicateDefinition<'db, 'diag, DB> { + fn range(&self) -> TextRange { + syntax_node_identifier_range(self.diag.definition, &self.db.parse(self.diag.file)) + } + + fn title(&self) -> String { + format!( + "a {} named `{}` has already been defined in this module", + self.value_or_type_string(), + self.diag.name, + ) + } + + fn primary_annotation(&self) -> Option { + Some(SourceAnnotation { + range: syntax_node_signature_range( + self.diag.definition, + &self.db.parse(self.diag.file), + ), + message: format!("`{}` redefined here", self.diag.name), + }) + } + + fn secondary_annotations(&self) -> Vec { + vec![SecondaryAnnotation { + range: InFile::new( + self.diag.file, + syntax_node_signature_range( + self.diag.first_definition, + &self.db.parse(self.diag.file), + ), + ), + message: format!( + "first definition of the {} `{}` here", + self.value_or_type_string(), + self.diag.name + ), + }] + } + + fn footer(&self) -> Vec { + vec![format!( + "`{}` must be defined only once in the {} namespace of this module", + self.diag.name, + self.value_or_type_string() + )] + } +} + +impl<'db, 'diag, DB: mun_hir::HirDatabase> DuplicateDefinition<'db, 'diag, DB> { + /// Returns either `type` or `value` definition on the type of definition. + fn value_or_type_string(&self) -> &'static str { + if self.diag.definition.kind() == SyntaxKind::STRUCT_DEF { + "type" + } else { + "value" + } + } +} + +impl<'db, 'diag, DB: mun_hir::HirDatabase> DuplicateDefinition<'db, 'diag, DB> { + /// Constructs a new instance of `DuplicateDefinition` + pub fn new(db: &'db DB, diag: &'diag mun_hir::diagnostics::DuplicateDefinition) -> Self { + DuplicateDefinition { db, diag } + } +} diff --git a/crates/mun_diagnostics/src/hir/expected_function.rs b/crates/mun_diagnostics/src/hir/expected_function.rs index 33ea6c6eb..b2310da6e 100644 --- a/crates/mun_diagnostics/src/hir/expected_function.rs +++ b/crates/mun_diagnostics/src/hir/expected_function.rs @@ -1,19 +1,27 @@ use super::HirDiagnostic; use crate::{Diagnostic, SourceAnnotation}; -use hir::HirDisplay; +use mun_hir::HirDisplay; use mun_syntax::TextRange; -pub struct ExpectedFunction<'db, 'diag, DB: hir::HirDatabase> { +/// An error that is emitted when a function is expected but something else is encountered: +/// +/// ```mun +/// # fn main() { +/// let a = 3; +/// let b = a(); // expected function +/// # } +/// ``` +pub struct ExpectedFunction<'db, 'diag, DB: mun_hir::HirDatabase> { db: &'db DB, - diag: &'diag hir::diagnostics::ExpectedFunction, + diag: &'diag mun_hir::diagnostics::ExpectedFunction, } -impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic for ExpectedFunction<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for ExpectedFunction<'db, 'diag, DB> { fn range(&self) -> TextRange { self.diag.highlight_range() } - fn label(&self) -> String { + fn title(&self) -> String { format!( "expected function, found `{}`", self.diag.found.display(self.db) @@ -28,9 +36,9 @@ impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic for ExpectedFunction<'db, 'dia } } -impl<'db, 'diag, DB: hir::HirDatabase> ExpectedFunction<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> ExpectedFunction<'db, 'diag, DB> { /// Constructs a new instance of `ExpectedFunction` - pub fn new(db: &'db DB, diag: &'diag hir::diagnostics::ExpectedFunction) -> Self { + pub fn new(db: &'db DB, diag: &'diag mun_hir::diagnostics::ExpectedFunction) -> Self { ExpectedFunction { db, diag } } } diff --git a/crates/mun_diagnostics/src/hir/mismatched_type.rs b/crates/mun_diagnostics/src/hir/mismatched_type.rs index c0fd2ba71..d9a5e7945 100644 --- a/crates/mun_diagnostics/src/hir/mismatched_type.rs +++ b/crates/mun_diagnostics/src/hir/mismatched_type.rs @@ -1,19 +1,30 @@ use super::HirDiagnostic; use crate::{Diagnostic, SourceAnnotation}; -use hir::HirDisplay; +use mun_hir::HirDisplay; use mun_syntax::TextRange; -pub struct MismatchedType<'db, 'diag, DB: hir::HirDatabase> { +/// An error that is emitted when a different type was found than expected. +/// +/// ```mun +/// fn add(a: i32, b: i32) -> i32{ +/// a+b +/// } +/// +/// # fn main() { +/// add(true, false); // type mismatch, expected i32 found bool. +/// # } +/// ``` +pub struct MismatchedType<'db, 'diag, DB: mun_hir::HirDatabase> { db: &'db DB, - diag: &'diag hir::diagnostics::MismatchedType, + diag: &'diag mun_hir::diagnostics::MismatchedType, } -impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic for MismatchedType<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for MismatchedType<'db, 'diag, DB> { fn range(&self) -> TextRange { self.diag.highlight_range() } - fn label(&self) -> String { + fn title(&self) -> String { format!( "expected `{}`, found `{}`", self.diag.expected.display(self.db), @@ -26,9 +37,9 @@ impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic for MismatchedType<'db, 'diag, } } -impl<'db, 'diag, DB: hir::HirDatabase> MismatchedType<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> MismatchedType<'db, 'diag, DB> { /// Constructs a new instance of `MismatchedType` - pub fn new(db: &'db DB, diag: &'diag hir::diagnostics::MismatchedType) -> Self { + pub fn new(db: &'db DB, diag: &'diag mun_hir::diagnostics::MismatchedType) -> Self { MismatchedType { db, diag } } } diff --git a/crates/mun_diagnostics/src/hir/missing_fields.rs b/crates/mun_diagnostics/src/hir/missing_fields.rs index 1947dd340..265b8d9d3 100644 --- a/crates/mun_diagnostics/src/hir/missing_fields.rs +++ b/crates/mun_diagnostics/src/hir/missing_fields.rs @@ -1,21 +1,32 @@ use super::HirDiagnostic; use crate::{Diagnostic, SourceAnnotation}; -use hir::HirDisplay; +use mun_hir::HirDisplay; use mun_syntax::{ast, AstNode, TextRange}; -pub struct MissingFields<'db, 'diag, DB: hir::HirDatabase> { +/// An error that is emitted when a field is missing from a struct initializer. +/// +/// ```mun +/// struct Foo { +/// a: i32, +/// } +/// +/// # fn main() { +/// let a = Foo {}; // missing field `a` +/// # } +/// ``` +pub struct MissingFields<'db, 'diag, DB: mun_hir::HirDatabase> { db: &'db DB, - diag: &'diag hir::diagnostics::MissingFields, + diag: &'diag mun_hir::diagnostics::MissingFields, location: TextRange, missing_fields: String, } -impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic for MissingFields<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for MissingFields<'db, 'diag, DB> { fn range(&self) -> TextRange { self.location } - fn label(&self) -> String { + fn title(&self) -> String { format!( "missing fields {} in initializer of `{}`", self.missing_fields, @@ -26,14 +37,14 @@ impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic for MissingFields<'db, 'diag, fn primary_annotation(&self) -> Option { Some(SourceAnnotation { range: self.location, - message: self.missing_fields.clone(), + message: format!("missing {}", self.missing_fields.clone()), }) } } -impl<'db, 'diag, DB: hir::HirDatabase> MissingFields<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> MissingFields<'db, 'diag, DB> { /// Constructs a new instance of `MissingFields` - pub fn new(db: &'db DB, diag: &'diag hir::diagnostics::MissingFields) -> Self { + pub fn new(db: &'db DB, diag: &'diag mun_hir::diagnostics::MissingFields) -> Self { let parse = db.parse(diag.file); let missing_fields = diag .field_names @@ -41,6 +52,7 @@ impl<'db, 'diag, DB: hir::HirDatabase> MissingFields<'db, 'diag, DB> { .map(|n| format!("`{}`", n)) .collect::>() .join(", "); + let location = ast::RecordLit::cast(diag.fields.to_node(&parse.syntax_node())) .and_then(|f| f.type_ref()) .map(|t| t.syntax().text_range()) diff --git a/crates/mun_diagnostics/src/hir/possibly_unitialized_variable.rs b/crates/mun_diagnostics/src/hir/possibly_unitialized_variable.rs index bb687d231..2f8d36f75 100644 --- a/crates/mun_diagnostics/src/hir/possibly_unitialized_variable.rs +++ b/crates/mun_diagnostics/src/hir/possibly_unitialized_variable.rs @@ -2,20 +2,28 @@ use super::HirDiagnostic; use crate::{Diagnostic, SourceAnnotation}; use mun_syntax::TextRange; -pub struct PossiblyUninitializedVariable<'db, 'diag, DB: hir::HirDatabase> { +/// An error that is emitted when trying to access a field that is potentially not yet initialized. +/// +/// ```mun +/// # fn main() { +/// let a; +/// let b = a; // `a` is possible not yet initialized +/// #} +/// ``` +pub struct PossiblyUninitializedVariable<'db, 'diag, DB: mun_hir::HirDatabase> { _db: &'db DB, - diag: &'diag hir::diagnostics::PossiblyUninitializedVariable, + diag: &'diag mun_hir::diagnostics::PossiblyUninitializedVariable, value_name: String, } -impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for PossiblyUninitializedVariable<'db, 'diag, DB> { fn range(&self) -> TextRange { self.diag.highlight_range() } - fn label(&self) -> String { + fn title(&self) -> String { format!("use of possibly-uninitialized `{}`", self.value_name) } @@ -24,9 +32,12 @@ impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic } } -impl<'db, 'diag, DB: hir::HirDatabase> PossiblyUninitializedVariable<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> PossiblyUninitializedVariable<'db, 'diag, DB> { /// Constructs a new instance of `PossiblyUninitializedVariable` - pub fn new(db: &'db DB, diag: &'diag hir::diagnostics::PossiblyUninitializedVariable) -> Self { + pub fn new( + db: &'db DB, + diag: &'diag mun_hir::diagnostics::PossiblyUninitializedVariable, + ) -> Self { let parse = db.parse(diag.file); // Get the text of the value as a string diff --git a/crates/mun_diagnostics/src/hir/unresolved_type.rs b/crates/mun_diagnostics/src/hir/unresolved_type.rs index 6d8156ae1..2ff0a13a7 100644 --- a/crates/mun_diagnostics/src/hir/unresolved_type.rs +++ b/crates/mun_diagnostics/src/hir/unresolved_type.rs @@ -2,18 +2,25 @@ use super::HirDiagnostic; use crate::{Diagnostic, SourceAnnotation}; use mun_syntax::{AstNode, TextRange}; -pub struct UnresolvedType<'db, 'diag, DB: hir::HirDatabase> { +/// An error that is emitted when trying to use a type that doesnt exist within the scope. +/// +/// ```mun +/// # fn main() { +/// let a = DoesntExist {}; // Cannot find `DoesntExist` in this scope. +/// #} +/// ``` +pub struct UnresolvedType<'db, 'diag, DB: mun_hir::HirDatabase> { _db: &'db DB, - diag: &'diag hir::diagnostics::UnresolvedType, + diag: &'diag mun_hir::diagnostics::UnresolvedType, value_name: String, } -impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic for UnresolvedType<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for UnresolvedType<'db, 'diag, DB> { fn range(&self) -> TextRange { self.diag.highlight_range() } - fn label(&self) -> String { + fn title(&self) -> String { format!("cannot find type `{}` in this scope", self.value_name) } @@ -25,9 +32,9 @@ impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic for UnresolvedType<'db, 'diag, } } -impl<'db, 'diag, DB: hir::HirDatabase> UnresolvedType<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> UnresolvedType<'db, 'diag, DB> { /// Constructs a new instance of `UnresolvedType` - pub fn new(db: &'db DB, diag: &'diag hir::diagnostics::UnresolvedType) -> Self { + pub fn new(db: &'db DB, diag: &'diag mun_hir::diagnostics::UnresolvedType) -> Self { let parse = db.parse(diag.file); // Get the text of the value as a string diff --git a/crates/mun_diagnostics/src/hir/unresolved_value.rs b/crates/mun_diagnostics/src/hir/unresolved_value.rs index d5315440e..8d94375f4 100644 --- a/crates/mun_diagnostics/src/hir/unresolved_value.rs +++ b/crates/mun_diagnostics/src/hir/unresolved_value.rs @@ -2,18 +2,25 @@ use super::HirDiagnostic; use crate::{Diagnostic, SourceAnnotation}; use mun_syntax::{AstNode, TextRange}; -pub struct UnresolvedValue<'db, 'diag, DB: hir::HirDatabase> { +/// An error that is emitted when trying to use a value that doesnt exist within the scope. +/// +/// ```mun +/// # fn main() { +/// let a = b; // Cannot find `b` in this scope. +/// #} +/// ``` +pub struct UnresolvedValue<'db, 'diag, DB: mun_hir::HirDatabase> { _db: &'db DB, - diag: &'diag hir::diagnostics::UnresolvedValue, + diag: &'diag mun_hir::diagnostics::UnresolvedValue, value_name: String, } -impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic for UnresolvedValue<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for UnresolvedValue<'db, 'diag, DB> { fn range(&self) -> TextRange { self.diag.highlight_range() } - fn label(&self) -> String { + fn title(&self) -> String { format!("cannot find value `{}` in this scope", self.value_name) } @@ -25,9 +32,9 @@ impl<'db, 'diag, DB: hir::HirDatabase> Diagnostic for UnresolvedValue<'db, 'diag } } -impl<'db, 'diag, DB: hir::HirDatabase> UnresolvedValue<'db, 'diag, DB> { +impl<'db, 'diag, DB: mun_hir::HirDatabase> UnresolvedValue<'db, 'diag, DB> { /// Constructs a new instance of `UnresolvedValue` - pub fn new(db: &'db DB, diag: &'diag hir::diagnostics::UnresolvedValue) -> Self { + pub fn new(db: &'db DB, diag: &'diag mun_hir::diagnostics::UnresolvedValue) -> Self { let parse = db.parse(diag.file); // Get the text of the value as a string diff --git a/crates/mun_diagnostics/src/lib.rs b/crates/mun_diagnostics/src/lib.rs index ebee143b7..f81aa41d9 100644 --- a/crates/mun_diagnostics/src/lib.rs +++ b/crates/mun_diagnostics/src/lib.rs @@ -1,5 +1,6 @@ mod hir; +use mun_hir::InFile; use mun_syntax::TextRange; ///! This crate provides in depth human readable diagnostic information and fixes for compiler @@ -19,17 +20,36 @@ pub struct SourceAnnotation { pub message: String, } +/// An annotation within the source code +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SecondaryAnnotation { + /// The location in the source + pub range: InFile, + + /// The message + pub message: String, +} + /// The base trait for all diagnostics in this crate. pub trait Diagnostic { + /// Returns the primary message of the diagnostic. + fn title(&self) -> String; + /// Returns the location of this diagnostic. fn range(&self) -> TextRange; - /// Returns the primary message of the diagnostic. - fn label(&self) -> String; - - /// Returns a source annotation that acts as the primary annotation for this Diagnostic. If this - /// function returns `None` use the values returned from [`range`] and [`label`]. + /// Returns a source annotation that acts as the primary annotation for this Diagnostic. fn primary_annotation(&self) -> Option; + + /// Returns secondary source annotation that are shown as additional references. + fn secondary_annotations(&self) -> Vec { + Vec::new() + } + + /// Optional footer text + fn footer(&self) -> Vec { + Vec::new() + } } pub trait DiagnosticFor { @@ -43,3 +63,12 @@ pub trait DiagnosticForWith { /// to perform lazy diagnostic evaluation. fn with_diagnostic R>(&self, with: &With, f: F) -> R; } + +impl Into for SecondaryAnnotation { + fn into(self) -> SourceAnnotation { + SourceAnnotation { + range: self.range.value, + message: self.message, + } + } +} diff --git a/crates/mun_hir/src/lib.rs b/crates/mun_hir/src/lib.rs index 468a6f6e9..604cd4401 100644 --- a/crates/mun_hir/src/lib.rs +++ b/crates/mun_hir/src/lib.rs @@ -54,6 +54,7 @@ pub use crate::{ LogicOp, Ordering, Pat, PatId, RecordLitField, Statement, UnaryOp, }, ids::ItemLoc, + in_file::InFile, input::{FileId, SourceRoot, SourceRootId}, name::Name, name_resolution::PerNs, diff --git a/crates/mun_language_server/src/diagnostics.rs b/crates/mun_language_server/src/diagnostics.rs index 8fec8bd8a..eeb4b29a6 100644 --- a/crates/mun_language_server/src/diagnostics.rs +++ b/crates/mun_language_server/src/diagnostics.rs @@ -1,13 +1,21 @@ use crate::db::AnalysisDatabase; +use hir::InFile; use hir::SourceDatabase; use mun_diagnostics::DiagnosticForWith; use mun_syntax::{Location, TextRange}; use std::cell::RefCell; +#[derive(Debug)] +pub struct SourceAnnotation { + pub message: String, + pub range: InFile, +} + #[derive(Debug)] pub struct Diagnostic { pub message: String, pub range: TextRange, + pub additional_annotations: Vec, // pub fix: Option, // pub severity: Severity, } @@ -29,17 +37,28 @@ pub(crate) fn diagnostics(db: &AnalysisDatabase, file_id: hir::FileId) -> Vec) { +async fn handle_diagnostics( + state: LanguageServerSnapshot, + mut sender: UnboundedSender, +) -> Cancelable<()> { // Iterate over all files for root in state.local_source_roots.iter() { // Get all the files - let files = match state.analysis.source_root_files(*root) { - Ok(files) => files, - Err(_) => return, - }; + let files = state.analysis.source_root_files(*root)?; // Publish all diagnostics for file in files { - let line_index = match state.analysis.file_line_index(file) { - Ok(line_index) => line_index, - Err(_) => return, - }; - let uri = state.file_id_to_uri(file); - let uri = uri.await.unwrap(); - let diagnostics = match state.analysis.diagnostics(file) { - Ok(line_index) => line_index, - Err(_) => return, + let line_index = state.analysis.file_line_index(file)?; + let uri = state.file_id_to_uri(file).await.unwrap(); + let diagnostics = state.analysis.diagnostics(file)?; + + let diagnostics = { + let mut lsp_diagnostics = Vec::with_capacity(diagnostics.len()); + for d in diagnostics { + lsp_diagnostics.push(lsp_types::Diagnostic { + range: convert_range(d.range, &line_index), + severity: Some(lsp_types::DiagnosticSeverity::Error), + code: None, + source: Some("mun".to_string()), + message: d.message, + related_information: { + let mut annotations = + Vec::with_capacity(d.additional_annotations.len()); + for annotation in d.additional_annotations { + annotations.push(lsp_types::DiagnosticRelatedInformation { + location: lsp_types::Location { + uri: state + .file_id_to_uri(annotation.range.file_id) + .await + .unwrap(), + range: convert_range( + annotation.range.value, + state + .analysis + .file_line_index(annotation.range.file_id)? + .deref(), + ), + }, + message: annotation.message, + }); + } + if annotations.is_empty() { + None + } else { + Some(annotations) + } + }, + tags: None, + }); + } + lsp_diagnostics }; - let diagnostics = diagnostics - .into_iter() - .map(|d| lsp_types::Diagnostic { - range: convert_range(d.range, &line_index), - severity: Some(lsp_types::DiagnosticSeverity::Error), - code: None, - source: Some("mun".to_string()), - message: d.message, - related_information: None, - tags: None, - }) - .collect(); - sender .send(Task::Notify(build_notification::( PublishDiagnosticsParams { @@ -425,6 +448,8 @@ async fn handle_diagnostics(state: LanguageServerSnapshot, mut sender: Unbounded .unwrap(); } } + + Ok(()) } /// Handles a task send by another async task