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..853d925be 100644 --- a/crates/mun_compiler/src/diagnostics_snippets.rs +++ b/crates/mun_compiler/src/diagnostics_snippets.rs @@ -1,363 +1,201 @@ -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( +/// Writes the specified syntax error to the output stream. +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..88148ba40 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 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 new file mode 100644 index 000000000..b7f7d5cf4 --- /dev/null +++ b/crates/mun_diagnostics/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "mun_diagnostics" +version = "0.1.0" +authors = ["The Mun Team "] +edition = "2018" +description = "Provides in-depth diagnostic information for compiler errors" +documentation = "https://docs.mun-lang.org/v0.2" +readme = "README.md" +homepage = "https://mun-lang.org" +repository = "https://github.com/mun-lang/mun" +license = "MIT OR Apache-2.0" +keywords = ["game", "hot-reloading", "language", "mun", "diagnostics"] +categories = ["game-development", "mun"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +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 new file mode 100644 index 000000000..644519e8b --- /dev/null +++ b/crates/mun_diagnostics/src/hir.rs @@ -0,0 +1,62 @@ +///! 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; +mod possibly_unitialized_variable; +mod unresolved_type; +mod unresolved_value; + +use crate::{Diagnostic, DiagnosticForWith, SourceAnnotation}; +use mun_hir::Diagnostic as HirDiagnostic; +use mun_syntax::TextRange; + +// Provides conversion of a mun_hir::Diagnostic to a crate::Diagnostic. This requires a database for +// most operations. +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::() { + f(&unresolved_value::UnresolvedValue::new(with, v)) + } else if let Some(v) = self.downcast_ref::() { + f(&unresolved_type::UnresolvedType::new(with, v)) + } else if let Some(v) = self.downcast_ref::() { + f(&expected_function::ExpectedFunction::new(with, v)) + } else if let Some(v) = self.downcast_ref::() { + f(&mismatched_type::MismatchedType::new(with, v)) + } else if let Some(v) = + self.downcast_ref::() + { + f(&possibly_unitialized_variable::PossiblyUninitializedVariable::new(with, v)) + } else if let Some(v) = self.downcast_ref::() { + f(&access_unknown_field::AccessUnknownField::new(with, v)) + } 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 }) + } + } +} + +/// Diagnostic handler for HIR diagnostics that do not have a specialized implementation. +struct GenericHirDiagnostic<'diag> { + diagnostic: &'diag dyn mun_hir::Diagnostic, +} + +impl<'diag> Diagnostic for GenericHirDiagnostic<'diag> { + fn range(&self) -> TextRange { + self.diagnostic.highlight_range() + } + + fn title(&self) -> String { + self.diagnostic.message() + } + + fn primary_annotation(&self) -> Option { + None + } +} diff --git a/crates/mun_diagnostics/src/hir/access_unknown_field.rs b/crates/mun_diagnostics/src/hir/access_unknown_field.rs new file mode 100644 index 000000000..780c2c7df --- /dev/null +++ b/crates/mun_diagnostics/src/hir/access_unknown_field.rs @@ -0,0 +1,56 @@ +use super::HirDiagnostic; +use crate::{Diagnostic, SourceAnnotation}; +use mun_hir::HirDisplay; +use mun_syntax::{ast, AstNode, TextRange}; + +/// An error that is emitted when trying to access a field that doesn't 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 mun_hir::diagnostics::AccessUnknownField, + location: TextRange, +} + +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for AccessUnknownField<'db, 'diag, DB> { + fn range(&self) -> TextRange { + self.location + } + + fn title(&self) -> String { + format!( + "no field `{}` on type `{}`", + self.diag.name, + self.diag.receiver_ty.display(self.db), + ) + } + + fn primary_annotation(&self) -> Option { + Some(SourceAnnotation { + range: self.location, + message: "unknown field".to_string(), + }) + } +} + +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 mun_hir::diagnostics::AccessUnknownField) -> Self { + let parse = db.parse(diag.file); + + let location = ast::FieldExpr::cast(diag.expr.to_node(&parse.syntax_node())) + .map(|f| f.field_range()) + .unwrap_or_else(|| diag.highlight_range()); + + AccessUnknownField { db, diag, location } + } +} 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 new file mode 100644 index 000000000..b2310da6e --- /dev/null +++ b/crates/mun_diagnostics/src/hir/expected_function.rs @@ -0,0 +1,44 @@ +use super::HirDiagnostic; +use crate::{Diagnostic, SourceAnnotation}; +use mun_hir::HirDisplay; +use mun_syntax::TextRange; + +/// 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 mun_hir::diagnostics::ExpectedFunction, +} + +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for ExpectedFunction<'db, 'diag, DB> { + fn range(&self) -> TextRange { + self.diag.highlight_range() + } + + fn title(&self) -> String { + format!( + "expected function, found `{}`", + self.diag.found.display(self.db) + ) + } + + fn primary_annotation(&self) -> Option { + Some(SourceAnnotation { + range: self.diag.highlight_range(), + message: "not a function".to_owned(), + }) + } +} + +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 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 new file mode 100644 index 000000000..d9a5e7945 --- /dev/null +++ b/crates/mun_diagnostics/src/hir/mismatched_type.rs @@ -0,0 +1,45 @@ +use super::HirDiagnostic; +use crate::{Diagnostic, SourceAnnotation}; +use mun_hir::HirDisplay; +use mun_syntax::TextRange; + +/// 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 mun_hir::diagnostics::MismatchedType, +} + +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for MismatchedType<'db, 'diag, DB> { + fn range(&self) -> TextRange { + self.diag.highlight_range() + } + + fn title(&self) -> String { + format!( + "expected `{}`, found `{}`", + self.diag.expected.display(self.db), + self.diag.found.display(self.db) + ) + } + + fn primary_annotation(&self) -> Option { + None + } +} + +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 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 new file mode 100644 index 000000000..265b8d9d3 --- /dev/null +++ b/crates/mun_diagnostics/src/hir/missing_fields.rs @@ -0,0 +1,68 @@ +use super::HirDiagnostic; +use crate::{Diagnostic, SourceAnnotation}; +use mun_hir::HirDisplay; +use mun_syntax::{ast, AstNode, TextRange}; + +/// 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 mun_hir::diagnostics::MissingFields, + location: TextRange, + missing_fields: String, +} + +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for MissingFields<'db, 'diag, DB> { + fn range(&self) -> TextRange { + self.location + } + + fn title(&self) -> String { + format!( + "missing fields {} in initializer of `{}`", + self.missing_fields, + self.diag.struct_ty.display(self.db) + ) + } + + fn primary_annotation(&self) -> Option { + Some(SourceAnnotation { + range: self.location, + message: format!("missing {}", self.missing_fields.clone()), + }) + } +} + +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 mun_hir::diagnostics::MissingFields) -> Self { + let parse = db.parse(diag.file); + let missing_fields = diag + .field_names + .iter() + .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()) + .unwrap_or_else(|| diag.highlight_range()); + + MissingFields { + db, + diag, + location, + missing_fields, + } + } +} diff --git a/crates/mun_diagnostics/src/hir/possibly_unitialized_variable.rs b/crates/mun_diagnostics/src/hir/possibly_unitialized_variable.rs new file mode 100644 index 000000000..2f8d36f75 --- /dev/null +++ b/crates/mun_diagnostics/src/hir/possibly_unitialized_variable.rs @@ -0,0 +1,52 @@ +use super::HirDiagnostic; +use crate::{Diagnostic, SourceAnnotation}; +use mun_syntax::TextRange; + +/// 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 mun_hir::diagnostics::PossiblyUninitializedVariable, + value_name: String, +} + +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic + for PossiblyUninitializedVariable<'db, 'diag, DB> +{ + fn range(&self) -> TextRange { + self.diag.highlight_range() + } + + fn title(&self) -> String { + format!("use of possibly-uninitialized `{}`", self.value_name) + } + + fn primary_annotation(&self) -> Option { + None + } +} + +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 mun_hir::diagnostics::PossiblyUninitializedVariable, + ) -> Self { + let parse = db.parse(diag.file); + + // Get the text of the value as a string + let value_name = diag.pat.to_node(&parse.syntax_node()).text().to_string(); + + PossiblyUninitializedVariable { + _db: db, + diag, + value_name, + } + } +} diff --git a/crates/mun_diagnostics/src/hir/unresolved_type.rs b/crates/mun_diagnostics/src/hir/unresolved_type.rs new file mode 100644 index 000000000..2ff0a13a7 --- /dev/null +++ b/crates/mun_diagnostics/src/hir/unresolved_type.rs @@ -0,0 +1,54 @@ +use super::HirDiagnostic; +use crate::{Diagnostic, SourceAnnotation}; +use mun_syntax::{AstNode, TextRange}; + +/// 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 mun_hir::diagnostics::UnresolvedType, + value_name: String, +} + +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for UnresolvedType<'db, 'diag, DB> { + fn range(&self) -> TextRange { + self.diag.highlight_range() + } + + fn title(&self) -> String { + format!("cannot find type `{}` in this scope", self.value_name) + } + + fn primary_annotation(&self) -> Option { + Some(SourceAnnotation { + range: self.diag.highlight_range(), + message: "not found in this scope".to_owned(), + }) + } +} + +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 mun_hir::diagnostics::UnresolvedType) -> Self { + let parse = db.parse(diag.file); + + // Get the text of the value as a string + let value_name = diag + .type_ref + .to_node(&parse.syntax_node()) + .syntax() + .text() + .to_string(); + + UnresolvedType { + _db: db, + diag, + value_name, + } + } +} diff --git a/crates/mun_diagnostics/src/hir/unresolved_value.rs b/crates/mun_diagnostics/src/hir/unresolved_value.rs new file mode 100644 index 000000000..8d94375f4 --- /dev/null +++ b/crates/mun_diagnostics/src/hir/unresolved_value.rs @@ -0,0 +1,49 @@ +use super::HirDiagnostic; +use crate::{Diagnostic, SourceAnnotation}; +use mun_syntax::{AstNode, TextRange}; + +/// 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 mun_hir::diagnostics::UnresolvedValue, + value_name: String, +} + +impl<'db, 'diag, DB: mun_hir::HirDatabase> Diagnostic for UnresolvedValue<'db, 'diag, DB> { + fn range(&self) -> TextRange { + self.diag.highlight_range() + } + + fn title(&self) -> String { + format!("cannot find value `{}` in this scope", self.value_name) + } + + fn primary_annotation(&self) -> Option { + Some(SourceAnnotation { + range: self.diag.highlight_range(), + message: "not found in this scope".to_owned(), + }) + } +} + +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 mun_hir::diagnostics::UnresolvedValue) -> Self { + let parse = db.parse(diag.file); + + // Get the text of the value as a string + let value_name = diag.expr.to_node(&parse.tree().syntax()).text().to_string(); + + UnresolvedValue { + _db: db, + diag, + value_name, + } + } +} diff --git a/crates/mun_diagnostics/src/lib.rs b/crates/mun_diagnostics/src/lib.rs new file mode 100644 index 000000000..a40bd9c1d --- /dev/null +++ b/crates/mun_diagnostics/src/lib.rs @@ -0,0 +1,77 @@ +mod hir; + +use mun_hir::InFile; +use mun_syntax::TextRange; + +///! This crate provides in-depth human-readable diagnostic information and fixes for compiler +///! errors that can be shared between the compiler and the language server. +///! +///! The processing of diagnostics into human-readable is separated from the machine-readable +///! diagnostics in for instance the HIR crate for performance reasons. This enables lazily querying +///! the system for more information only when required. + +/// An annotation within the source code +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SourceAnnotation { + /// The location in the source + pub range: TextRange, + + /// The message + 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 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() + } +} + +/// When implemented enables requesting `Diagnostic`s for the implementer. +pub trait DiagnosticFor { + /// Calls the specified function `f` with an instance of a [`Diagnostic`]. This can be used + /// to perform lazy diagnostic evaluation. + fn with_diagnostic R>(&self, f: F) -> R; +} + +/// Like [`DiagnosticFor`], enables requesting `Diagnostic`s for the implementer but only if passed +/// a required object. +pub trait DiagnosticForWith { + /// Calls the specified function `f` with an instance of a [`Diagnostic`]. This can be used + /// 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/diagnostics.rs b/crates/mun_hir/src/diagnostics.rs index 848ef9da8..633d978db 100644 --- a/crates/mun_hir/src/diagnostics.rs +++ b/crates/mun_hir/src/diagnostics.rs @@ -433,6 +433,7 @@ impl Diagnostic for FieldCountMismatch { pub struct MissingFields { pub file: FileId, pub fields: SyntaxNodePtr, + pub struct_ty: Ty, pub field_names: Vec, } diff --git a/crates/mun_hir/src/lib.rs b/crates/mun_hir/src/lib.rs index c37127544..604cd4401 100644 --- a/crates/mun_hir/src/lib.rs +++ b/crates/mun_hir/src/lib.rs @@ -47,12 +47,14 @@ pub use crate::{ DefDatabase, DefDatabaseStorage, HirDatabase, HirDatabaseStorage, SourceDatabase, SourceDatabaseStorage, Upcast, }, + diagnostics::{Diagnostic, DiagnosticSink}, display::HirDisplay, expr::{ resolver_for_expr, ArithOp, BinaryOp, Body, CmpOp, Expr, ExprId, ExprScopes, Literal, 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_hir/src/ty.rs b/crates/mun_hir/src/ty.rs index 9a30b8a03..e5781b5a7 100644 --- a/crates/mun_hir/src/ty.rs +++ b/crates/mun_hir/src/ty.rs @@ -177,6 +177,14 @@ impl Ty { _ => None, }) } + + /// Returns true if this instance represents a known type. + pub fn is_known(&self) -> bool { + match self { + Ty::Unknown => false, + _ => true, + } + } } /// A list of substitutions for generic parameters. diff --git a/crates/mun_hir/src/ty/infer.rs b/crates/mun_hir/src/ty/infer.rs index 29905aa04..6d25ba431 100644 --- a/crates/mun_hir/src/ty/infer.rs +++ b/crates/mun_hir/src/ty/infer.rs @@ -404,7 +404,7 @@ impl<'a> InferenceResultBuilder<'a> { self.infer_expr(*expr, &Expectation::has_type(ty.clone())); } if let Some(s) = ty.as_struct() { - self.check_record_lit(tgt_expr, s, &fields); + self.check_record_lit(tgt_expr, &ty, s, &fields); } ty } @@ -631,7 +631,13 @@ impl<'a> InferenceResultBuilder<'a> { } // Checks whether the passed fields match the fields of a struct definition. - fn check_record_lit(&mut self, tgt_expr: ExprId, expected: Struct, fields: &[RecordLitField]) { + fn check_record_lit( + &mut self, + tgt_expr: ExprId, + ty: &Ty, + expected: Struct, + fields: &[RecordLitField], + ) { let struct_data = expected.data(self.db.upcast()); if struct_data.kind != StructKind::Record { self.diagnostics @@ -660,6 +666,7 @@ impl<'a> InferenceResultBuilder<'a> { if !missed_fields.is_empty() { self.diagnostics.push(InferenceDiagnostic::MissingFields { id: tgt_expr, + struct_ty: ty.clone(), names: missed_fields, }); } @@ -1069,6 +1076,7 @@ mod diagnostics { }, MissingFields { id: ExprId, + struct_ty: Ty, names: Vec, }, MismatchedStructLit { @@ -1302,7 +1310,11 @@ mod diagnostics { found: *found, }) } - InferenceDiagnostic::MissingFields { id, names } => { + InferenceDiagnostic::MissingFields { + id, + struct_ty, + names, + } => { let fields = body .expr_syntax(*id) .unwrap() @@ -1311,6 +1323,7 @@ mod diagnostics { sink.push(MissingFields { file, + struct_ty: struct_ty.clone(), fields, field_names: names.to_vec(), }); diff --git a/crates/mun_language_server/Cargo.toml b/crates/mun_language_server/Cargo.toml index 644aaf6f7..0a2bf6dad 100644 --- a/crates/mun_language_server/Cargo.toml +++ b/crates/mun_language_server/Cargo.toml @@ -31,3 +31,4 @@ rayon = "1.3" num_cpus = "1.13.0" mun_target = { version = "=0.2.0", path = "../mun_target" } mun_syntax = { version = "=0.2.0", path = "../mun_syntax" } +mun_diagnostics = { version = "=0.1.0", path = "../mun_diagnostics" } diff --git a/crates/mun_language_server/src/diagnostics.rs b/crates/mun_language_server/src/diagnostics.rs index c4bd67b18..eeb4b29a6 100644 --- a/crates/mun_language_server/src/diagnostics.rs +++ b/crates/mun_language_server/src/diagnostics.rs @@ -1,12 +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, } @@ -26,17 +35,30 @@ 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