Explorar el Código

Fancy colored compiler diagnostics using codespan-reporting

Signed-off-by: Sean Young <sean@mess.org>
Sean Young hace 3 años
padre
commit
f8507191eb
Se han modificado 10 ficheros con 168 adiciones y 78 borrados
  1. 1 0
      Cargo.toml
  2. 10 5
      src/bin/solang.rs
  3. 1 1
      src/file_resolver.rs
  4. 128 51
      src/sema/diagnostics.rs
  5. 20 14
      src/sema/file.rs
  6. 1 1
      tests/ewasm.rs
  7. 2 2
      tests/imports.rs
  8. 1 1
      tests/solana.rs
  9. 1 1
      tests/solana_tests/simple.rs
  10. 3 2
      tests/substrate.rs

+ 1 - 0
Cargo.toml

@@ -43,6 +43,7 @@ num-rational = "0.4"
 indexmap = "1.8"
 once_cell = "1.9"
 solang-parser = { path = "solang-parser", version = "0.1.1" }
+codespan-reporting = "0.11"
 
 [dev-dependencies]
 num-derive = "0.3"

+ 10 - 5
src/bin/solang.rs

@@ -285,7 +285,7 @@ fn main() {
         for filename in matches.values_of_os("INPUT").unwrap() {
             let ns = solang::parse_and_resolve(filename, &mut resolver, target);
 
-            diagnostics::print_messages(&resolver, &ns, verbose);
+            diagnostics::print_diagnostics(&resolver, &ns, verbose);
 
             if ns.contracts.is_empty() {
                 eprintln!("{}: error: no contracts found", filename.to_string_lossy());
@@ -334,8 +334,13 @@ fn main() {
         }
 
         if errors {
-            eprintln!("error: not all contracts are valid");
-            std::process::exit(1);
+            if matches.is_present("STD-JSON") {
+                println!("{}", serde_json::to_string(&json).unwrap());
+                std::process::exit(0);
+            } else {
+                eprintln!("error: not all contracts are valid");
+                std::process::exit(1);
+            }
         }
 
         if target == solang::Target::Solana {
@@ -442,10 +447,10 @@ fn process_file(
     codegen(&mut ns, opt);
 
     if matches.is_present("STD-JSON") {
-        let mut out = diagnostics::message_as_json(&ns, resolver);
+        let mut out = diagnostics::diagnostics_as_json(&ns, resolver);
         json.errors.append(&mut out);
     } else {
-        diagnostics::print_messages(resolver, &ns, verbose);
+        diagnostics::print_diagnostics(resolver, &ns, verbose);
     }
 
     if let Some("ast-dot") = matches.value_of("EMIT") {

+ 1 - 1
src/file_resolver.rs

@@ -81,7 +81,7 @@ impl FileResolver {
 
     /// Get file with contents. This must be a file which was previously
     /// add to the cache
-    pub fn get_file_contents_and_number(&mut self, file: &Path) -> (Arc<str>, usize) {
+    pub fn get_file_contents_and_number(&self, file: &Path) -> (Arc<str>, usize) {
         let file_no = self.cached_paths[file];
 
         (self.files[file_no].clone(), file_no)

+ 128 - 51
src/sema/diagnostics.rs

@@ -1,64 +1,45 @@
 use super::ast::{Diagnostic, Level, Namespace};
 use crate::file_resolver::FileResolver;
+use codespan_reporting::{diagnostic, files, term};
 use serde::Serialize;
+use std::{io, sync::Arc};
 
-fn formatted_message(diagnostic: &Diagnostic, ns: &Namespace, cache: &FileResolver) -> String {
-    let mut s = if let Some(pos) = diagnostic.pos {
-        let loc = ns.files[pos.0].loc_to_string(&pos);
-
-        let (full_line, beg_line_no, beg_offset, type_size) =
-            cache.get_line_and_offset_from_loc(&ns.files[pos.0], &pos);
-
-        format!(
-            "{}: {}: {}\nLine {}:\n\t{}\n\t{:-<7$}{:^<8$}",
-            loc,
-            diagnostic.level.to_string(),
-            diagnostic.message,
-            beg_line_no + 1,
-            full_line,
-            "",
-            "",
-            beg_offset,
-            type_size
-        )
-    } else {
-        format!(
-            "solang: {}: {}",
-            diagnostic.level.to_string(),
-            diagnostic.message
-        )
-    };
+/// Print the diagnostics to stderr with fancy formatting
+pub fn print_diagnostics(cache: &FileResolver, ns: &Namespace, debug: bool) {
+    let (files, file_id) = convert_files(ns, cache);
 
-    for note in &diagnostic.notes {
-        let loc = ns.files[note.pos.0].loc_to_string(&note.pos);
-
-        let (full_line, beg_line_no, beg_offset, type_size) =
-            cache.get_line_and_offset_from_loc(&ns.files[note.pos.0], &note.pos);
-
-        s.push_str(&format!(
-            "\n\t{}: {}: {}\n\tLine {}:\n\t\t{}\n\t\t{:-<7$}{:^<8$}",
-            loc,
-            "note",
-            note.message,
-            beg_line_no + 1,
-            full_line,
-            "",
-            "",
-            beg_offset,
-            type_size
-        ));
-    }
+    let writer = term::termcolor::StandardStream::stderr(term::termcolor::ColorChoice::Always);
+    let config = term::Config::default();
+
+    for msg in &ns.diagnostics {
+        if msg.level == Level::Debug && !debug {
+            continue;
+        }
 
-    s
+        let diagnostic = convert_diagnostic(msg, &file_id);
+
+        term::emit(&mut writer.lock(), &config, &files, &diagnostic).unwrap();
+    }
 }
 
-pub fn print_messages(cache: &FileResolver, ns: &Namespace, debug: bool) {
+/// Print the diagnostics to stdout with plain formatting
+pub fn print_diagnostics_plain(cache: &FileResolver, ns: &Namespace, debug: bool) {
+    let (files, file_id) = convert_files(ns, cache);
+
+    let config = term::Config::default();
+
     for msg in &ns.diagnostics {
-        if !debug && msg.level == Level::Debug {
+        if msg.level == Level::Debug && !debug {
             continue;
         }
 
-        eprintln!("{}", formatted_message(msg, ns, cache));
+        let diagnostic = convert_diagnostic(msg, &file_id);
+
+        let mut buffer = RawBuffer::new();
+
+        term::emit(&mut buffer, &config, &files, &diagnostic).unwrap();
+
+        println!("{}", buffer.into_string());
     }
 }
 
@@ -67,6 +48,50 @@ pub fn any_errors(diagnotic: &[Diagnostic]) -> bool {
     diagnotic.iter().any(|m| m.level == Level::Error)
 }
 
+fn convert_diagnostic(msg: &Diagnostic, file_id: &[usize]) -> diagnostic::Diagnostic<usize> {
+    let diagnostic = diagnostic::Diagnostic::new(match msg.level {
+        Level::Debug => diagnostic::Severity::Help,
+        Level::Info => diagnostic::Severity::Note,
+        Level::Error => diagnostic::Severity::Error,
+        Level::Warning => diagnostic::Severity::Warning,
+    })
+    .with_message(msg.message.to_owned());
+
+    let mut labels = Vec::new();
+
+    if let Some(pos) = msg.pos {
+        labels.push(diagnostic::Label::primary(file_id[pos.0], pos.1..pos.2));
+    }
+
+    for note in &msg.notes {
+        labels.push(
+            diagnostic::Label::secondary(file_id[note.pos.0], note.pos.1..note.pos.2)
+                .with_message(note.message.to_owned()),
+        );
+    }
+
+    if labels.is_empty() {
+        diagnostic
+    } else {
+        diagnostic.with_labels(labels)
+    }
+}
+
+fn convert_files(
+    ns: &Namespace,
+    cache: &FileResolver,
+) -> (files::SimpleFiles<String, Arc<str>>, Vec<usize>) {
+    let mut files = files::SimpleFiles::new();
+    let mut file_id = Vec::new();
+
+    for file in &ns.files {
+        let (contents, _) = cache.get_file_contents_and_number(&file.path);
+        file_id.push(files.add(format!("{}", file), contents.to_owned()));
+    }
+
+    (files, file_id)
+}
+
 #[derive(Serialize)]
 pub struct LocJson {
     pub file: String,
@@ -86,14 +111,26 @@ pub struct OutputJson {
     pub formattedMessage: String,
 }
 
-pub fn message_as_json(ns: &Namespace, cache: &FileResolver) -> Vec<OutputJson> {
+pub fn diagnostics_as_json(ns: &Namespace, cache: &FileResolver) -> Vec<OutputJson> {
+    let (files, file_id) = convert_files(ns, cache);
     let mut json = Vec::new();
 
+    let config = term::Config {
+        display_style: term::DisplayStyle::Short,
+        ..Default::default()
+    };
+
     for msg in &ns.diagnostics {
         if msg.level == Level::Info || msg.level == Level::Debug {
             continue;
         }
 
+        let diagnostic = convert_diagnostic(msg, &file_id);
+
+        let mut buffer = RawBuffer::new();
+
+        term::emit(&mut buffer, &config, &files, &diagnostic).unwrap();
+
         let location = msg.pos.map(|pos| LocJson {
             file: format!("{}", ns.files[pos.0].path.display()),
             start: pos.1 + 1,
@@ -106,9 +143,49 @@ pub fn message_as_json(ns: &Namespace, cache: &FileResolver) -> Vec<OutputJson>
             component: "general".to_owned(),
             severity: msg.level.to_string().to_owned(),
             message: msg.message.to_owned(),
-            formattedMessage: formatted_message(msg, ns, cache),
+            formattedMessage: buffer.into_string(),
         });
     }
 
     json
 }
+
+pub struct RawBuffer {
+    buf: Vec<u8>,
+}
+
+impl RawBuffer {
+    #[allow(clippy::new_without_default)]
+    pub fn new() -> RawBuffer {
+        RawBuffer { buf: Vec::new() }
+    }
+
+    pub fn into_string(self) -> String {
+        String::from_utf8(self.buf).unwrap()
+    }
+}
+
+impl io::Write for RawBuffer {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        self.buf.extend(buf);
+        Ok(buf.len())
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        Ok(())
+    }
+}
+
+impl term::termcolor::WriteColor for RawBuffer {
+    fn supports_color(&self) -> bool {
+        false
+    }
+
+    fn set_color(&mut self, _: &term::termcolor::ColorSpec) -> io::Result<()> {
+        Ok(())
+    }
+
+    fn reset(&mut self) -> io::Result<()> {
+        Ok(())
+    }
+}

+ 20 - 14
src/sema/file.rs

@@ -1,9 +1,9 @@
 use super::ast::File;
 use crate::parser::pt::Loc;
-use std::path::{Path, PathBuf};
+use std::{fmt, path};
 
 impl File {
-    pub fn new(path: PathBuf, contents: &str, cache_no: usize) -> Self {
+    pub fn new(path: path::PathBuf, contents: &str, cache_no: usize) -> Self {
         let mut line_starts = Vec::new();
 
         for (ind, c) in contents.char_indices() {
@@ -24,17 +24,12 @@ impl File {
         let (from_line, from_column) = self.offset_to_line_column(loc.1);
         let (to_line, to_column) = self.offset_to_line_column(loc.2);
 
-        #[cfg(windows)]
-        let path = fix_windows_verbatim(&self.path);
-        #[cfg(not(windows))]
-        let path: &Path = &self.path;
-
         if from_line == to_line && from_column == to_column {
-            format!("{}:{}:{}", path.display(), from_line + 1, from_column + 1)
+            format!("{}:{}:{}", self, from_line + 1, from_column + 1)
         } else if from_line == to_line {
             format!(
                 "{}:{}:{}-{}",
-                path.display(),
+                self,
                 from_line + 1,
                 from_column + 1,
                 to_column + 1
@@ -42,7 +37,7 @@ impl File {
         } else {
             format!(
                 "{}:{}:{}-{}:{}",
-                path.display(),
+                self,
                 from_line + 1,
                 from_column + 1,
                 to_line + 1,
@@ -85,13 +80,24 @@ impl File {
     }
 }
 
+impl fmt::Display for File {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        #[cfg(not(windows))]
+        let res = write!(f, "{}", self.path.display());
+
+        #[cfg(windows)]
+        let res = write!(f, "{}", fix_windows_verbatim(&self.path).display());
+
+        res
+    }
+}
+
 /// Windows verbatim paths look like \\?\C:\foo\bar which not very human readable,
 /// so fix up paths. This is a copy of fn fix_windows_verbatim_for_gcc in rust
 /// https://github.com/rust-lang/rust/blob/master/compiler/rustc_fs_util/src/lib.rs#L23
 #[cfg(windows)]
-pub fn fix_windows_verbatim(p: &Path) -> PathBuf {
+fn fix_windows_verbatim(p: &path::Path) -> path::PathBuf {
     use std::ffi::OsString;
-    use std::path;
     let mut components = p.components();
     let prefix = match components.next() {
         Some(path::Component::Prefix(p)) => p,
@@ -101,7 +107,7 @@ pub fn fix_windows_verbatim(p: &Path) -> PathBuf {
         path::Prefix::VerbatimDisk(disk) => {
             let mut base = OsString::from(format!("{}:", disk as char));
             base.push(components.as_path());
-            PathBuf::from(base)
+            path::PathBuf::from(base)
         }
         path::Prefix::VerbatimUNC(server, share) => {
             let mut base = OsString::from(r"\\");
@@ -109,7 +115,7 @@ pub fn fix_windows_verbatim(p: &Path) -> PathBuf {
             base.push(r"\");
             base.push(share);
             base.push(components.as_path());
-            PathBuf::from(base)
+            path::PathBuf::from(base)
         }
         _ => p.to_path_buf(),
     }

+ 1 - 1
tests/ewasm.rs

@@ -823,7 +823,7 @@ fn build_solidity(src: &str) -> TestRuntime {
         false,
     );
 
-    diagnostics::print_messages(&cache, &ns, false);
+    diagnostics::print_diagnostics_plain(&cache, &ns, false);
 
     for v in &res {
         println!("contract size:{}", v.0.len());

+ 2 - 2
tests/imports.rs

@@ -78,7 +78,7 @@ fn import_map() {
 
     println!("stderr: {}", stderr);
 
-    assert!(stderr.contains("import_map.sol:1:8-21: error: file not found ‘foo/bar.sol’"));
+    assert!(stderr.contains("file not found ‘foo/bar.sol’"));
 }
 
 #[test]
@@ -110,5 +110,5 @@ fn import() {
 
     println!("stderr: {}", stderr);
 
-    assert!(stderr.contains("error: file not found ‘bar.sol’"));
+    assert!(stderr.contains("file not found ‘bar.sol’"));
 }

+ 1 - 1
tests/solana.rs

@@ -124,7 +124,7 @@ fn build_solidity(src: &str) -> VirtualMachine {
     // codegen all the contracts; some additional errors/warnings will be detected here
     codegen(&mut ns, &Options::default());
 
-    diagnostics::print_messages(&cache, &ns, false);
+    diagnostics::print_diagnostics_plain(&cache, &ns, false);
 
     let context = inkwell::context::Context::create();
 

+ 1 - 1
tests/solana_tests/simple.rs

@@ -277,7 +277,7 @@ contract line {
 
     let ns = solang::parse_and_resolve(OsStr::new("test.sol"), &mut cache, Target::Solana);
 
-    solang::sema::diagnostics::print_messages(&cache, &ns, false);
+    solang::sema::diagnostics::print_diagnostics_plain(&cache, &ns, false);
 
     assert_eq!(
         ns.diagnostics[1].message,

+ 3 - 2
tests/substrate.rs

@@ -1190,7 +1190,8 @@ pub fn build_solidity(src: &'static str) -> TestRuntime {
         false,
     );
 
-    diagnostics::print_messages(&cache, &ns, false);
+    diagnostics::print_diagnostics_plain(&cache, &ns, false);
+
     no_errors(ns.diagnostics);
 
     assert!(!res.is_empty());
@@ -1227,7 +1228,7 @@ pub fn build_solidity_with_overflow_check(src: &'static str) -> TestRuntime {
         true,
     );
 
-    diagnostics::print_messages(&cache, &ns, false);
+    diagnostics::print_diagnostics_plain(&cache, &ns, false);
 
     assert!(!res.is_empty());