diff --git a/crates/mun/src/diagnostic.rs b/crates/mun/src/diagnostic.rs deleted file mode 100644 index 51fba9726..000000000 --- a/crates/mun/src/diagnostic.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::line_index::{LineCol, LineIndex}; -use colored::*; -use mun_errors::Diagnostic; -use std::fmt; - -pub trait Emit { - fn emit(&self, line_index: &LineIndex); -} - -impl Emit for Diagnostic { - fn emit(&self, line_index: &LineIndex) { - let line_col = line_index.line_col(self.loc.offset()); - println!("{} ({}:{}): {}", - "error".red(), - line_col.line + 1, - line_col.col, - self.message); - } -} \ No newline at end of file diff --git a/crates/mun/src/main.rs b/crates/mun/src/main.rs index fe91120fe..c2b14b660 100644 --- a/crates/mun/src/main.rs +++ b/crates/mun/src/main.rs @@ -2,11 +2,12 @@ extern crate failure; use std::cell::RefCell; +use std::env; use std::rc::Rc; use std::time::Duration; use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; -use mun_compiler::{Config, PathOrInline, Target}; +use mun_compiler::{Config, DisplayColor, PathOrInline, Target}; use mun_runtime::{invoke_fn, ReturnTypeReflection, Runtime, RuntimeBuilder}; fn main() -> Result<(), failure::Error> { @@ -40,6 +41,13 @@ fn main() -> Result<(), failure::Error> { .takes_value(true) .help("target triple for which code is compiled"), ) + .arg( + Arg::with_name("color") + .long("color") + .takes_value(true) + .possible_values(&["enable", "auto", "disable"]) + .help("color text in terminal"), + ) .about("Compiles a local Mun file into a module"), ) .subcommand( @@ -136,6 +144,17 @@ fn compiler_options(matches: &ArgMatches) -> Result return Err(format_err!("Only optimization levels 0-3 are supported")), }; + let display_color = matches + .value_of("color") + .map(ToOwned::to_owned) + .or_else(|| env::var("MUN_TERMINAL_COLOR").ok()) + .map(|value| match value.as_str() { + "disable" => DisplayColor::Disable, + "enable" => DisplayColor::Enable, + _ => DisplayColor::Auto, + }) + .unwrap_or(DisplayColor::Auto); + Ok(mun_compiler::CompilerOptions { input: PathOrInline::Path(matches.value_of("INPUT").unwrap().into()), // Safe because its a required arg config: Config { @@ -144,6 +163,7 @@ fn compiler_options(matches: &ArgMatches) -> Result 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():float{\n48\n}"; + let line_index: LineIndex = LineIndex::new(source_code); + + insta::assert_debug_snapshot!(SliceBuilder::new(true) + .origin("/tmp/usr/test.mun") + .source_annotation((14, 20), "test source annotation", AnnotationType::Note) + .build(source_code, &line_index)); + } + #[test] + fn snippet_builder_snapshot() { + let source_code = "fn foo():float{\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 d9d770e42..84465a376 100644 --- a/crates/mun_compiler/src/diagnostics.rs +++ b/crates/mun_compiler/src/diagnostics.rs @@ -1,113 +1,219 @@ -use mun_hir::diagnostics::{Diagnostic as HirDiagnostic, DiagnosticSink}; -use mun_hir::{FileId, HirDatabase, HirDisplay, Module}; -use mun_syntax::{ast, AstNode, SyntaxKind}; +use mun_hir::diagnostics::DiagnosticSink; +use mun_hir::{FileId, HirDatabase, Module}; + use std::cell::RefCell; -mod emit; +use annotate_snippets::snippet::Snippet; -pub use emit::Emit; -use mun_errors::{Diagnostic, Level}; +use crate::diagnostics_snippets; /// Constructs diagnostic messages for the given file. -pub fn diagnostics(db: &impl HirDatabase, file_id: FileId) -> Vec { +pub fn diagnostics(db: &impl 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).display().to_string(); - result.extend(parse.errors().iter().map(|err| Diagnostic { - level: Level::Error, - loc: err.location(), - message: format!("Syntax Error: {}", err), + 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(Diagnostic { - level: Level::Error, - loc: d.highlight_range().into(), - message: d.message(), - }); + result + .borrow_mut() + .push(diagnostics_snippets::generic_error( + d, + db, + &parse, + &relative_file_path, + &source_code, + &line_index, + )); }) .on::(|d| { - let text = d.expr.to_node(&parse.syntax_node()).text().to_string(); - result.borrow_mut().push(Diagnostic { - level: Level::Error, - loc: d.highlight_range().into(), - message: format!("could not find value `{}` in this scope", text), - }); + result + .borrow_mut() + .push(diagnostics_snippets::unresolved_value_error( + d, + db, + &parse, + &relative_file_path, + &source_code, + &line_index, + )); }) .on::(|d| { - let text = d - .type_ref - .to_node(&parse.syntax_node()) - .syntax() - .text() - .to_string(); - result.borrow_mut().push(Diagnostic { - level: Level::Error, - loc: d.highlight_range().into(), - message: format!("could not find type `{}` in this scope", text), - }); + 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(Diagnostic { - level: Level::Error, - loc: d.highlight_range().into(), - message: format!("expected function, found `{}`", d.found.display(db)), - }); + 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(Diagnostic { - level: Level::Error, - loc: d.highlight_range().into(), - message: format!( - "expected `{}`, found `{}`", - d.expected.display(db), - d.found.display(db) - ), - }); + 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(Diagnostic { - level: Level::Error, - loc: match d.definition.kind() { - SyntaxKind::FUNCTION_DEF => { - ast::FunctionDef::cast(d.definition.to_node(&parse.syntax_node())) - .map(|f| f.signature_range()) - .unwrap_or_else(|| d.highlight_range()) - .into() - } - _ => d.highlight_range().into(), - }, - message: d.message(), - }); + 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(Diagnostic { - level: Level::Error, - loc: d.highlight_range().into(), - message: format!( - "use of possibly-uninitialized variable: `{}`", - d.pat.to_node(&parse.syntax_node()).text().to_string() - ), - }) + 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(Diagnostic { - level: Level::Error, - loc: ast::FieldExpr::cast(d.expr.to_node(&parse.syntax_node())) - .map(|f| f.field_range()) - .unwrap_or_else(|| d.highlight_range()) - .into(), - message: format!( - "no field `{}` on type `{}`", - d.name, - d.receiver_ty.display(db), - ), - }) + 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}; + + /// Compile passed source code and return all compilation errors + fn compilation_errors(source_code: &str) -> String { + let config = Config { + display_color: DisplayColor::Disable, + ..Config::default() + }; + + let input = PathOrInline::Inline { + rel_path: RelativePathBuf::from("main.mun"), + contents: source_code.to_owned(), + }; + + let (driver, _) = Driver::with_file(config, input).unwrap(); + + let mut compilation_errors = Vec::::new(); + + let _ = driver.emit_diagnostics(&mut compilation_errors).unwrap(); + + String::from_utf8(compilation_errors).unwrap() + } + + #[test] + fn test_syntax_error() { + insta::assert_display_snapshot!(compilation_errors("\n\nfn main(\n struct Foo\n")); + } + + #[test] + fn test_unresolved_value_error() { + insta::assert_display_snapshot!(compilation_errors( + "\n\nfn main() {\nlet b = a;\n\nlet d = c;\n}" + )); + } + + #[test] + fn test_unresolved_type_error() { + insta::assert_display_snapshot!(compilation_errors( + "\n\nfn main() {\nlet a = Foo{};\n\nlet b = Bar{};\n}" + )); + } + + #[test] + fn test_expected_function_error() { + insta::assert_display_snapshot!(compilation_errors( + "\n\nfn main() {\nlet a = Foo();\n\nlet b = Bar();\n}" + )); + } + + #[test] + fn test_mismatched_type_error() { + insta::assert_display_snapshot!(compilation_errors( + "\n\nfn main() {\nlet a: float = false;\n\nlet b: bool = 22;\n}" + )); + } + + #[test] + fn test_duplicate_definition_error() { + insta::assert_display_snapshot!(compilation_errors( + "\n\nfn foo(){}\n\nfn foo(){}\n\nstruct Bar;\n\nstruct Bar;\n\nfn BAZ(){}\n\nstruct BAZ;" + )); + } + + #[test] + fn test_possibly_uninitialized_variable_error() { + insta::assert_display_snapshot!(compilation_errors( + "\n\nfn main() {\nlet a;\nif 5>6 {\na = 5\n}\nlet b = a;\n}" + )); + } + + #[test] + fn test_access_unknown_field_error() { + insta::assert_display_snapshot!(compilation_errors( + "\n\nstruct Foo {\ni: bool\n}\n\nfn main() {\nlet a = Foo { i: false };\nlet b = a.t;\n}" + )); + } +} diff --git a/crates/mun_compiler/src/diagnostics/emit.rs b/crates/mun_compiler/src/diagnostics/emit.rs deleted file mode 100644 index a7f329b96..000000000 --- a/crates/mun_compiler/src/diagnostics/emit.rs +++ /dev/null @@ -1,101 +0,0 @@ -use mun_errors::Diagnostic; -use mun_hir::{FileId, SourceDatabase}; -use std::io; -use termcolor::{Color, ColorSpec, WriteColor}; - -pub trait Emit { - fn emit( - &self, - writer: &mut impl WriteColor, - db: &impl SourceDatabase, - file_id: FileId, - ) -> io::Result<()>; -} - -impl Emit for Diagnostic { - fn emit( - &self, - writer: &mut impl WriteColor, - db: &impl SourceDatabase, - file_id: FileId, - ) -> io::Result<()> { - let line_index = db.line_index(file_id); - let text = db.file_text(file_id).to_string(); - let path = db.file_relative_path(file_id); - let line_col = line_index.line_col(self.loc.offset()); - let line_col_end = line_index.line_col(self.loc.end_offset()); - - let header = ColorSpec::new() - .set_fg(Some(Color::White)) - .set_bold(true) - .set_intense(true) - .clone(); - let error = header.clone().set_fg(Some(Color::Red)).clone(); - let snippet_gutter = ColorSpec::new() - .set_fg(Some(Color::Cyan)) - .set_bold(true) - .set_intense(true) - .clone(); - let snippet_text = ColorSpec::new(); - - // Write severity name - writer.set_color(&error)?; - write!(writer, "error")?; - - // Write diagnostic message - writer.set_color(&header)?; - writeln!(writer, ": {}", self.message)?; - - if let Some(snippet) = line_index.line_str(line_col.line, &text) { - // Determine gutter offset - let line_str = format!("{}", line_col.line + 1); - let gutter_indent = " ".to_string().repeat(line_str.len()); - - writer.set_color(&snippet_gutter)?; - write!(writer, "{}-->", gutter_indent)?; - writer.set_color(&snippet_text)?; - writeln!( - writer, - " {}:{}:{}", - path.as_str(), - line_col.line + 1, - line_col.col - )?; - - // Snippet - writer.set_color(&snippet_gutter)?; - writeln!(writer, "{} |", gutter_indent)?; - write!(writer, "{} | ", line_str)?; - writer.set_color(&snippet_text)?; - writeln!(writer, "{}", snippet)?; - - writer.set_color(&snippet_gutter)?; - write!(writer, "{} |", gutter_indent)?; - writer.set_color(&error)?; - - if line_col.line == line_col_end.line { - // single-line diagnostic - writeln!( - writer, - " {}{}", - " ".to_string().repeat(line_col.col as usize), - "^".to_string() - .repeat((line_col_end.col - line_col.col) as usize) - )?; - } else { - // move cursor to next line - writeln!(writer)?; - } - } - - // // Write the start location - // println!(" {} {}:{}:{}", - // "-->".cyan(), - // path.as_str(), - // line_col.line + 1, - // line_col.col); - - writer.reset()?; - Ok(()) - } -} diff --git a/crates/mun_compiler/src/diagnostics_snippets.rs b/crates/mun_compiler/src/diagnostics_snippets.rs new file mode 100644 index 000000000..b9c809559 --- /dev/null +++ b/crates/mun_compiler/src/diagnostics_snippets.rs @@ -0,0 +1,363 @@ +use mun_hir::diagnostics::Diagnostic as HirDiagnostic; +use mun_hir::{HirDatabase, HirDisplay}; +use mun_syntax::{ + ast, AstNode, Parse, SourceFile, SyntaxError, SyntaxKind, SyntaxNodePtr, TextRange, +}; + +use std::sync::Arc; + +use mun_hir::line_index::LineIndex; + +use crate::annotate::{AnnotationBuilder, SliceBuilder, SnippetBuilder}; + +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( + syntax_error: &SyntaxError, + _: &impl 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 +} + +pub(crate) fn generic_error( + diagnostic: &dyn HirDiagnostic, + _: &impl 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() +} + +pub(crate) fn unresolved_value_error( + diagnostic: &mun_hir::diagnostics::UnresolvedValue, + _: &impl 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, + _: &impl 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: &impl 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: &impl 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, + _: &impl 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, + _: &impl 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: &impl 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::*; + + #[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)); + } +} diff --git a/crates/mun_compiler/src/driver.rs b/crates/mun_compiler/src/driver.rs index 4849391b5..1f641eab5 100644 --- a/crates/mun_compiler/src/driver.rs +++ b/crates/mun_compiler/src/driver.rs @@ -1,20 +1,23 @@ //! `Driver` is a stateful compiler frontend that enables incremental compilation by retaining state //! from previous compilation. -use crate::{ - db::CompilerDatabase, - diagnostics::{diagnostics, Emit}, - PathOrInline, -}; +use crate::{db::CompilerDatabase, diagnostics::diagnostics, PathOrInline}; use mun_codegen::{IrDatabase, ModuleBuilder}; use mun_hir::{FileId, RelativePathBuf, SourceDatabase, SourceRoot, SourceRootId}; + use std::{path::PathBuf, sync::Arc}; mod config; +mod display_color; pub use self::config::Config; -use mun_errors::{Diagnostic, Level}; -use termcolor::WriteColor; +pub use self::display_color::DisplayColor; + +use annotate_snippets::{ + display_list::DisplayList, + formatter::DisplayListFormatter, + snippet::{AnnotationType, Snippet}, +}; pub const WORKSPACE: SourceRootId = SourceRootId(0); @@ -22,6 +25,7 @@ pub const WORKSPACE: SourceRootId = SourceRootId(0); pub struct Driver { db: CompilerDatabase, out_dir: Option, + display_color: DisplayColor, } impl Driver { @@ -30,6 +34,7 @@ impl Driver { let mut driver = Driver { db: CompilerDatabase::new(), out_dir: None, + display_color: config.display_color, }; // Move relevant configuration into the database @@ -93,7 +98,7 @@ impl Driver { impl Driver { /// Returns a vector containing all the diagnostic messages for the project. - pub fn diagnostics(&self) -> Vec { + pub fn diagnostics(&self) -> Vec { self.db .source_root(WORKSPACE) .files() @@ -104,14 +109,21 @@ impl Driver { /// Emits all diagnostic messages currently in the database; returns true if errors were /// emitted. - pub fn emit_diagnostics(&self, writer: &mut impl WriteColor) -> Result { + pub fn emit_diagnostics( + &self, + writer: &mut dyn std::io::Write, + ) -> Result { let mut has_errors = false; + let dlf = DisplayListFormatter::new(self.display_color.should_enable(), false); for file_id in self.db.source_root(WORKSPACE).files() { let diags = diagnostics(&self.db, file_id); - for diagnostic in diags.iter() { - diagnostic.emit(writer, &self.db, file_id)?; - if diagnostic.level == Level::Error { - has_errors = true; + for diagnostic in diags { + let dl = DisplayList::from(diagnostic.clone()); + writeln!(writer, "{}", dlf.format(&dl)).unwrap(); + if let Some(annotation) = diagnostic.title { + if let AnnotationType::Error = annotation.annotation_type { + has_errors = true; + } } } } diff --git a/crates/mun_compiler/src/driver/config.rs b/crates/mun_compiler/src/driver/config.rs index 1ac29a657..649601fa8 100644 --- a/crates/mun_compiler/src/driver/config.rs +++ b/crates/mun_compiler/src/driver/config.rs @@ -1,3 +1,4 @@ +use crate::DisplayColor; pub use mun_codegen::OptimizationLevel; use mun_target::spec::Target; use std::path::PathBuf; @@ -14,6 +15,9 @@ pub struct Config { /// The optional output directory to store all outputs. If no directory is specified all output /// is stored in a temporary directory. pub out_dir: Option, + + /// Whether or not to use colors in terminal output + pub display_color: DisplayColor, } impl Default for Config { @@ -25,6 +29,7 @@ impl Default for Config { target: target.unwrap(), optimization_lvl: OptimizationLevel::Default, out_dir: None, + display_color: DisplayColor::Auto, } } } diff --git a/crates/mun_compiler/src/driver/display_color.rs b/crates/mun_compiler/src/driver/display_color.rs new file mode 100644 index 000000000..7dd61829c --- /dev/null +++ b/crates/mun_compiler/src/driver/display_color.rs @@ -0,0 +1,77 @@ +use std::env; +#[cfg(target_os = "windows")] +use std::process::Command; + +#[derive(Debug, Clone, Copy)] +pub enum DisplayColor { + Disable, + Auto, + Enable, +} + +impl DisplayColor { + pub(crate) fn should_enable(self) -> bool { + match self { + DisplayColor::Disable => false, + DisplayColor::Auto => terminal_support_ansi(), + DisplayColor::Enable => true, + } + } +} + +/// Decides whether the current terminal supports ANSI escape codes based on the `term` environment variable and the operating system. +fn terminal_support_ansi() -> bool { + match env::var("term") { + Ok(terminal) => match terminal.as_str() { + "dumb" => false, + _ => true, + }, + Err(_) => { + #[cfg(target_os = "windows")] + return cmd_supports_ansi(); + #[cfg(not(target_os = "windows"))] + return false; + } + } +} + +#[cfg(target_os = "windows")] +/// Determines whether the 'cmd' supports ANSI escape codes, based on the user's version of Windows. +fn cmd_supports_ansi() -> bool { + // Run `ver` program to find out Windows version + Command::new("cmd") + .args(&["/C", "ver"]) + .output() + .map_or(false, |output| { + String::from_utf8(output.stdout).map_or(false, |windows_version| { + let windows_version = windows_version + .split(' ') // split to drop "Microsoft", "Windows" and "[Version" from string + .last() // latest element contains Windows version with noisy ']' char + .and_then(|window_version| { + let mut window_version: String = window_version.trim().to_string(); + + // Remove ']' char + window_version.pop(); + + let window_version: Vec<&str> = window_version.split('.').collect(); + + Some(( + window_version[0].parse::(), + window_version[1].parse::(), + window_version[2].parse::(), + )) + }); + + if let Some((Ok(major), Ok(minor), Ok(patch))) = windows_version { + // From Windows 10.0.10586 version and higher ANSI escape codes work in `cmd` + let windows_support_ansi = major >= 10 && (patch >= 10586 || minor > 0); + if windows_support_ansi { + let _ = ansi_term::enable_ansi_support(); + } + windows_support_ansi + } else { + false + } + }) + }) +} diff --git a/crates/mun_compiler/src/lib.rs b/crates/mun_compiler/src/lib.rs index effd3097d..5069f91bd 100644 --- a/crates/mun_compiler/src/lib.rs +++ b/crates/mun_compiler/src/lib.rs @@ -1,18 +1,22 @@ #![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. mod diagnostics; +mod diagnostics_snippets; mod driver; pub use mun_hir::{FileId, RelativePath, RelativePathBuf}; pub use mun_target::spec::Target; use std::path::{Path, PathBuf}; -pub use termcolor::{ColorChoice, StandardStream}; +pub use crate::driver::DisplayColor; pub use crate::driver::{Config, Driver}; +pub use annotate::{AnnotationBuilder, SliceBuilder, SnippetBuilder}; pub use mun_codegen::OptimizationLevel; +use std::io::stderr; + #[derive(Debug, Clone)] pub enum PathOrInline { Path(PathBuf), @@ -56,8 +60,7 @@ impl CompilerOptions { pub fn main(options: CompilerOptions) -> Result, failure::Error> { let (mut driver, file_id) = Driver::with_file(options.config, options.input)?; - let mut writer = StandardStream::stderr(ColorChoice::Auto); - if driver.emit_diagnostics(&mut writer)? { + if driver.emit_diagnostics(&mut stderr())? { Ok(None) } else { driver.write_assembly(file_id).map(Some) diff --git a/crates/mun_compiler/src/snapshots/mun_compiler__annotate__tests__annotation_builder_snapshot.snap b/crates/mun_compiler/src/snapshots/mun_compiler__annotate__tests__annotation_builder_snapshot.snap new file mode 100644 index 000000000..930201d4a --- /dev/null +++ b/crates/mun_compiler/src/snapshots/mun_compiler__annotate__tests__annotation_builder_snapshot.snap @@ -0,0 +1,13 @@ +--- +source: crates/mun_compiler/src/annotate.rs +expression: "AnnotationBuilder::new(AnnotationType::Note).id(\"1\").label(\"test annotation\").build()" +--- +Annotation { + id: Some( + "1", + ), + label: Some( + "test annotation", + ), + annotation_type: Note, +} diff --git a/crates/mun_compiler/src/snapshots/mun_compiler__annotate__tests__slice_builder_snapshot.snap b/crates/mun_compiler/src/snapshots/mun_compiler__annotate__tests__slice_builder_snapshot.snap new file mode 100644 index 000000000..e96ba9223 --- /dev/null +++ b/crates/mun_compiler/src/snapshots/mun_compiler__annotate__tests__slice_builder_snapshot.snap @@ -0,0 +1,22 @@ +--- +source: crates/mun_compiler/src/annotate.rs +expression: "SliceBuilder::new(true).origin(\"/tmp/usr/test.mun\").source_annotation((14,\n 20),\n \"test source annotation\",\n AnnotationType::Note).build(source_code,\n &line_index)" +--- +Slice { + source: "fn foo():float{\n48\n}", + line_start: 1, + origin: Some( + "/tmp/usr/test.mun", + ), + annotations: [ + SourceAnnotation { + range: ( + 14, + 22, + ), + label: "test source annotation", + annotation_type: Note, + }, + ], + fold: true, +} diff --git a/crates/mun_compiler/src/snapshots/mun_compiler__annotate__tests__snippet_builder_snapshot.snap b/crates/mun_compiler/src/snapshots/mun_compiler__annotate__tests__snippet_builder_snapshot.snap new file mode 100644 index 000000000..7e7be9491 --- /dev/null +++ b/crates/mun_compiler/src/snapshots/mun_compiler__annotate__tests__snippet_builder_snapshot.snap @@ -0,0 +1,56 @@ +--- +source: crates/mun_compiler/src/annotate.rs +expression: "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,\n 20),\n \"test source annotation\",\n AnnotationType::Note).source_annotation((35,\n 41),\n \"test source annotation\",\n AnnotationType::Error).build(source_code,\n &line_index)).build()" +--- +Snippet { + title: Some( + Annotation { + id: Some( + "1", + ), + label: Some( + "test annotation", + ), + annotation_type: Note, + }, + ), + footer: [ + Annotation { + id: Some( + "2", + ), + label: Some( + "test annotation", + ), + annotation_type: Warning, + }, + ], + slices: [ + Slice { + source: "fn foo():float{\n48\n}\n\nfn bar():bool{\n23\n}", + line_start: 1, + origin: Some( + "/tmp/usr/test.mun", + ), + annotations: [ + SourceAnnotation { + range: ( + 14, + 22, + ), + label: "test source annotation", + annotation_type: Note, + }, + SourceAnnotation { + range: ( + 39, + 47, + ), + label: "test source annotation", + annotation_type: Error, + }, + ], + fold: true, + }, + ], +} 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 new file mode 100644 index 000000000..92aa1969f --- /dev/null +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__access_unknown_field_error.snap @@ -0,0 +1,11 @@ +--- +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 + | +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 new file mode 100644 index 000000000..cfe4538b6 --- /dev/null +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__duplicate_definition_error.snap @@ -0,0 +1,42 @@ +--- +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 + | +3 | fn foo(){} + | -------- previous 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 + | +... +7 | struct Bar; + | ---------- previous 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 + | + 3 | fn foo(){} + 4 | + 5 | fn foo(){} + 6 | +... +10 | +11 | fn BAZ(){} + | -------- previous definition of the value `BAZ` here +12 | +13 | struct BAZ; + | ^^^^^^^^^^ `BAZ` redefined here + | + = note: `BAZ` must be defined only once in the value 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 new file mode 100644 index 000000000..c5601c14e --- /dev/null +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__expected_function_error.snap @@ -0,0 +1,29 @@ +--- +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 + | +4 | let a = Foo(); + | ^^^ not found in this scope + | +error: expected function type + --> main.mun:4:8 + | +4 | let a = Foo(); + | ^^^ expected function, found `{unknown}` + | +error: cannot find value `Bar` in this scope + --> main.mun:6:8 + | +6 | let b = Bar(); + | ^^^ not found in this scope + | +error: expected function type + --> main.mun:6:8 + | +6 | let b = Bar(); + | ^^^ expected function, found `{unknown}` + | + 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 new file mode 100644 index 000000000..e54d6a9fa --- /dev/null +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__mismatched_type_error.snap @@ -0,0 +1,17 @@ +--- +source: crates/mun_compiler/src/diagnostics.rs +expression: "compilation_errors(\"\\n\\nfn main() {\\nlet a: float = false;\\n\\nlet b: bool = 22;\\n}\")" +--- +error: mismatched type + --> main.mun:4:15 + | +4 | let a: float = false; + | ^^^^^ expected `float`, found `bool` + | +error: mismatched type + --> main.mun:6:14 + | +6 | let b: bool = 22; + | ^^ expected `bool`, found `int` + | + 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 new file mode 100644 index 000000000..de09cfe41 --- /dev/null +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__possibly_uninitialized_variable_error.snap @@ -0,0 +1,11 @@ +--- +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 + | +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 new file mode 100644 index 000000000..2f2fc5bc1 --- /dev/null +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__syntax_error.snap @@ -0,0 +1,29 @@ +--- +source: crates/mun_compiler/src/diagnostics.rs +expression: "compilation_errors(\"\\n\\nfn main(\\n struct Foo\\n\")" +--- +error: syntax error + --> main.mun:3:8 + | +3 | fn main( + | ^ expected value parameter + | +error: syntax error + --> main.mun:3:8 + | +3 | fn main( + | ^ expected R_PAREN + | +error: syntax error + --> main.mun:3:8 + | +3 | fn main( + | ^ expected a block + | +error: syntax error + --> main.mun:4:11 + | +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 new file mode 100644 index 000000000..faadb682f --- /dev/null +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__unresolved_type_error.snap @@ -0,0 +1,17 @@ +--- +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 + | +4 | let a = Foo{}; + | ^^^ not found in this scope + | +error: cannot find type `Bar` in this scope + --> main.mun:6:8 + | +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 new file mode 100644 index 000000000..3d92526e5 --- /dev/null +++ b/crates/mun_compiler/src/snapshots/mun_compiler__diagnostics__tests__unresolved_value_error.snap @@ -0,0 +1,17 @@ +--- +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 + | +4 | let b = a; + | ^ not found in this scope + | +error: cannot find value `c` in this scope + --> main.mun:6:8 + | +6 | let d = c; + | ^ not found in this scope + | + diff --git a/crates/mun_compiler_daemon/src/lib.rs b/crates/mun_compiler_daemon/src/lib.rs index bb2c4319d..d545df2a3 100644 --- a/crates/mun_compiler_daemon/src/lib.rs +++ b/crates/mun_compiler_daemon/src/lib.rs @@ -2,9 +2,11 @@ use std::sync::mpsc::channel; use std::time::Duration; use failure::Error; -use mun_compiler::{ColorChoice, CompilerOptions, Driver, PathOrInline, StandardStream}; +use mun_compiler::{CompilerOptions, Driver, PathOrInline}; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; +use std::io::stderr; + pub fn main(options: CompilerOptions) -> Result<(), Error> { // Need to canonicalize path to do comparisons let input_path = match &options.input { @@ -21,8 +23,7 @@ pub fn main(options: CompilerOptions) -> Result<(), Error> { let (mut driver, file_id) = Driver::with_file(options.config, options.input)?; // Compile at least once - let mut writer = StandardStream::stderr(ColorChoice::Auto); - if !driver.emit_diagnostics(&mut writer)? { + if !driver.emit_diagnostics(&mut stderr())? { driver.write_assembly(file_id)?; } @@ -32,7 +33,7 @@ pub fn main(options: CompilerOptions) -> Result<(), Error> { Ok(Write(ref path)) | Ok(Create(ref path)) if path == &input_path => { let contents = std::fs::read_to_string(path)?; driver.set_file_text(file_id, &contents); - if !driver.emit_diagnostics(&mut writer)? { + if !driver.emit_diagnostics(&mut stderr())? { driver.write_assembly(file_id)?; println!("Successfully compiled: {}", path.display()) } diff --git a/crates/mun_errors/Cargo.toml b/crates/mun_errors/Cargo.toml deleted file mode 100644 index 7515970d9..000000000 --- a/crates/mun_errors/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "mun_errors" -version = "0.2.0" -authors = ["The Mun Team "] -edition = "2018" -homepage = "https://mun-lang.org" -repository = "https://github.com/mun-lang/mun" -license = "MIT OR Apache-2.0" -description = "Error handling functionality for Mun" - -[dependencies] -text_unit = { version = "0.1.6", features = ["serde"] } diff --git a/crates/mun_errors/LICENSE-APACHE b/crates/mun_errors/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a..000000000 --- a/crates/mun_errors/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/mun_errors/LICENSE-MIT b/crates/mun_errors/LICENSE-MIT deleted file mode 120000 index b2cfbdc7b..000000000 --- a/crates/mun_errors/LICENSE-MIT +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-MIT \ No newline at end of file diff --git a/crates/mun_errors/src/lib.rs b/crates/mun_errors/src/lib.rs deleted file mode 100644 index 94227dc9b..000000000 --- a/crates/mun_errors/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod location; - -pub use crate::location::Location; - -/// Defines the severity of diagnostics. -/// TODO: Contains only Error, for now, maybe add some more? -#[derive(Clone, Copy, Debug, PartialEq, Hash)] -pub enum Level { - Error, -} - -#[derive(Clone, Debug, PartialEq, Hash)] -pub struct Diagnostic { - pub level: Level, - pub loc: location::Location, - pub message: String, -} diff --git a/crates/mun_errors/src/location.rs b/crates/mun_errors/src/location.rs deleted file mode 100644 index 2e15c55ce..000000000 --- a/crates/mun_errors/src/location.rs +++ /dev/null @@ -1,42 +0,0 @@ -use text_unit::{TextRange, TextUnit}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Location { - Offset(TextUnit), - Range(TextRange), -} - -impl Into for TextUnit { - fn into(self) -> Location { - Location::Offset(self) - } -} - -impl Into for TextRange { - fn into(self) -> Location { - Location::Range(self) - } -} - -impl Location { - pub fn offset(&self) -> TextUnit { - match &self { - Location::Offset(offset) => *offset, - Location::Range(range) => range.start(), - } - } - - pub fn end_offset(&self) -> TextUnit { - match &self { - Location::Offset(offset) => *offset, - Location::Range(range) => range.end(), - } - } - - pub fn add_offset(&self, plus_offset: TextUnit, minus_offset: TextUnit) -> Location { - match &self { - Location::Range(range) => Location::Range(range + plus_offset - minus_offset), - Location::Offset(offset) => Location::Offset(offset + plus_offset - minus_offset), - } - } -} diff --git a/crates/mun_hir/src/line_index.rs b/crates/mun_hir/src/line_index.rs index 88f51fce4..a4c67d5fd 100644 --- a/crates/mun_hir/src/line_index.rs +++ b/crates/mun_hir/src/line_index.rs @@ -38,17 +38,29 @@ impl LineIndex { } } - pub fn line_str<'a>(&self, line: u32, text: &'a str) -> Option<&'a str> { - let start_of_line = self.newlines.get(line as usize)?.to_usize(); - let end_of_line = self + /// Retrieves the text between `first_line` and `last_line`, if any. + pub fn text_part<'a>( + &self, + first_line: u32, + last_line: u32, + text: &'a str, + text_len: usize, + ) -> Option<&'a str> { + let start_of_part = self.newlines.get(first_line as usize)?.to_usize(); + let end_of_part = self .newlines - .get((line + 1) as usize) + .get(last_line as usize + 1) .map(|u| u.to_usize() - 1) - .unwrap_or(text.len() as usize); - Some(&text[start_of_line..end_of_line]) + .unwrap_or(text_len); + Some(&text[start_of_part..end_of_part]) } -} + /// Retrieves the offset to the line corresponding to `line_index`. + #[inline] + pub fn line_offset(&self, line_index: u32) -> usize { + self.newlines[line_index as usize].to_usize() + } +} #[cfg(test)] mod tests { use super::*; @@ -64,12 +76,24 @@ mod tests { assert_eq!(index.line_col(7.into()), LineCol { line: 1, col: 1 }); } #[test] - fn test_line_str() { + fn test_text_part() { let text = "ℱ٥ℜ\n†ěṦτ\nℙน尺קő$ع"; + let text_len = text.len(); + let index = LineIndex::new(text); + assert_eq!(index.text_part(0, 0, &text, text_len), Some("ℱ٥ℜ")); + assert_eq!(index.text_part(0, 1, &text, text_len), Some("ℱ٥ℜ\n†ěṦτ")); + assert_eq!( + index.text_part(1, 2, &text, text_len), + Some("†ěṦτ\nℙน尺קő$ع") + ); + assert_eq!(index.text_part(0, 2, &text, text_len), Some(text)); + } + #[test] + fn test_line_offset() { + let text = "for\ntest\npurpose"; let index = LineIndex::new(text); - assert_eq!(index.line_str(0, &text), Some("ℱ٥ℜ")); - assert_eq!(index.line_str(1, &text), Some("†ěṦτ")); - assert_eq!(index.line_str(2, &text), Some("ℙน尺קő$ع")); - assert_eq!(index.line_str(3, &text), None); + assert_eq!(index.line_offset(0), 0); + assert_eq!(index.line_offset(1), 4); + assert_eq!(index.line_offset(2), 9); } } diff --git a/crates/mun_runtime/src/test.rs b/crates/mun_runtime/src/test.rs index b515a68a1..f24abc82f 100644 --- a/crates/mun_runtime/src/test.rs +++ b/crates/mun_runtime/src/test.rs @@ -2,14 +2,15 @@ use crate::function::IntoFunctionInfo; use crate::{ ArgumentReflection, RetryResultExt, ReturnTypeReflection, Runtime, RuntimeBuilder, StructRef, }; -use mun_compiler::{ColorChoice, Config, Driver, FileId, PathOrInline, RelativePathBuf}; +use mun_compiler::{Config, Driver, FileId, PathOrInline, RelativePathBuf}; use std::cell::RefCell; -use std::io::Write; use std::path::PathBuf; use std::rc::Rc; use std::thread::sleep; use std::time::Duration; +use std::io::stderr; + /// Implements a compiler and runtime in one that can invoke functions. Use of the TestDriver /// enables quick testing of Mun constructs in the runtime with hot-reloading support. struct TestDriver { @@ -51,9 +52,7 @@ impl TestDriver { contents: text.to_owned(), }; let (mut driver, file_id) = Driver::with_file(config, input).unwrap(); - let mut err_stream = mun_compiler::StandardStream::stderr(ColorChoice::Auto); - if driver.emit_diagnostics(&mut err_stream).unwrap() { - err_stream.flush().unwrap(); + if driver.emit_diagnostics(&mut stderr()).unwrap() { panic!("compiler errors..") } let out_path = driver.write_assembly(file_id).unwrap(); diff --git a/crates/mun_runtime_capi/src/tests.rs b/crates/mun_runtime_capi/src/tests.rs index edfd70769..a39cc2249 100644 --- a/crates/mun_runtime_capi/src/tests.rs +++ b/crates/mun_runtime_capi/src/tests.rs @@ -1,7 +1,9 @@ use crate::{error::*, *}; -use mun_compiler::{ColorChoice, Config, Driver, PathOrInline, RelativePathBuf}; +use mun_compiler::{Config, Driver, PathOrInline, RelativePathBuf}; use std::{ffi::CString, mem, path::Path, ptr}; +use std::io::stderr; + /// Combines a compiler and runtime in one. Use of the TestDriver allows for quick testing of Mun /// constructs in the runtime with hot-reloading support. struct TestDriver { @@ -22,8 +24,7 @@ impl TestDriver { contents: text.to_owned(), }; let (mut driver, file_id) = Driver::with_file(config, input).unwrap(); - let mut err_stream = mun_compiler::StandardStream::stderr(ColorChoice::Auto); - if driver.emit_diagnostics(&mut err_stream).unwrap() { + if driver.emit_diagnostics(&mut stderr()).unwrap() { panic!("compiler errors..") } let out_path = driver.write_assembly(file_id).unwrap(); diff --git a/crates/mun_syntax/Cargo.toml b/crates/mun_syntax/Cargo.toml index e38f74697..0bac5aecf 100644 --- a/crates/mun_syntax/Cargo.toml +++ b/crates/mun_syntax/Cargo.toml @@ -15,7 +15,6 @@ text_unit = { version = "0.1.6", features = ["serde"] } smol_str = { version = "0.1.12", features = ["serde"] } unicode-xid = "0.1.0" drop_bomb = "0.1.4" -mun_errors = { path = "../mun_errors"} [dev-dependencies] insta = "0.12.0" diff --git a/crates/mun_syntax/src/ast/extensions.rs b/crates/mun_syntax/src/ast/extensions.rs index ce5251b0e..9bc17da7c 100644 --- a/crates/mun_syntax/src/ast/extensions.rs +++ b/crates/mun_syntax/src/ast/extensions.rs @@ -147,6 +147,25 @@ impl ast::StructDef { pub fn kind(&self) -> StructKind { StructKind::from_node(self) } + pub fn signature_range(&self) -> TextRange { + let struct_kw = self + .syntax() + .children_with_tokens() + .find(|p| p.kind() == T![struct]) + .map(|kw| kw.text_range()); + let name = self.name().map(|n| n.syntax.text_range()); + + let start = struct_kw + .map(|kw| kw.start()) + .unwrap_or_else(|| self.syntax.text_range().start()); + + let end = name + .map(|name| name.end()) + .or_else(|| struct_kw.map(|kw| kw.end())) + .unwrap_or_else(|| self.syntax().text_range().end()); + + TextRange::from_to(start, end) + } } pub enum VisibilityKind { diff --git a/crates/mun_syntax/src/syntax_error.rs b/crates/mun_syntax/src/syntax_error.rs index 823b9c580..e3ace2a27 100644 --- a/crates/mun_syntax/src/syntax_error.rs +++ b/crates/mun_syntax/src/syntax_error.rs @@ -1,7 +1,49 @@ use crate::parsing::ParseError; -use mun_errors::Location; use std::fmt; +use text_unit::{TextRange, TextUnit}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Location { + Offset(TextUnit), + Range(TextRange), +} + +impl Into for TextUnit { + fn into(self) -> Location { + Location::Offset(self) + } +} + +impl Into for TextRange { + fn into(self) -> Location { + Location::Range(self) + } +} + +impl Location { + pub fn offset(&self) -> TextUnit { + match &self { + Location::Offset(offset) => *offset, + Location::Range(range) => range.start(), + } + } + + pub fn end_offset(&self) -> TextUnit { + match &self { + Location::Offset(offset) => *offset, + Location::Range(range) => range.end(), + } + } + + pub fn add_offset(&self, plus_offset: TextUnit, minus_offset: TextUnit) -> Location { + match &self { + Location::Range(range) => Location::Range(range + plus_offset - minus_offset), + Location::Offset(offset) => Location::Offset(offset + plus_offset - minus_offset), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct SyntaxError { kind: SyntaxErrorKind,