From 214e3bc4b675b08ce4df76b1373f4548949e67ee Mon Sep 17 00:00:00 2001 From: Akshay Date: Tue, 19 Oct 2021 15:58:46 +0530 Subject: fully flesh out CLI --- Cargo.lock | 306 +++++++++++++++++++++++++++++++++++++++++++++++++++++- bin/Cargo.toml | 5 +- bin/src/config.rs | 170 ++++++++++++++++++++++++++++++ bin/src/err.rs | 28 +++++ bin/src/main.rs | 112 ++++++++------------ bin/src/traits.rs | 83 +++++++++++++++ 6 files changed, 632 insertions(+), 72 deletions(-) create mode 100644 bin/src/config.rs create mode 100644 bin/src/err.rs create mode 100644 bin/src/traits.rs diff --git a/Cargo.lock b/Cargo.lock index 8b371ea..69e4495 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,13 @@ version = 3 [[package]] -name = "anyhow" -version = "1.0.44" +name = "aho-corasick" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] [[package]] name = "ariadne" @@ -17,12 +20,38 @@ dependencies = [ "yansi", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + [[package]] name = "cbitset" version = "0.2.0" @@ -32,24 +61,114 @@ dependencies = [ "num-traits", ] +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.0.0-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcd70aa5597dbc42f7217a543f9ef2768b2ef823ba29036072d30e1d88e98406" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5bb0d655624a0b8770d1c178fb8ffcb1f91cc722cb08f451e3dc72465421ac" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "countme" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "328b822bdcba4d4e402be8d9adb6eebf269f969f8eadef977a553ff3c4fbcb58" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "globset" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + [[package]] name = "hashbrown" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "if_chain" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown 0.11.2", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -67,6 +186,21 @@ dependencies = [ "rowan", ] +[[package]] +name = "libc" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + [[package]] name = "macros" version = "0.1.0" @@ -76,6 +210,12 @@ dependencies = [ "syn", ] +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + [[package]] name = "memoffset" version = "0.6.4" @@ -94,6 +234,36 @@ dependencies = [ "autocfg", ] +[[package]] +name = "os_str_bytes" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.29" @@ -112,6 +282,23 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "rnix" version = "0.9.0" @@ -130,7 +317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1b36e449f3702f3b0c821411db1cbdf30fb451726a9456dce5dabcd44420043" dependencies = [ "countme", - "hashbrown", + "hashbrown 0.9.1", "memoffset", "rustc-hash", "text-size", @@ -152,12 +339,21 @@ checksum = "b203e79e90905594272c1c97c7af701533d42adaab0beb3859018e477d54a3b0" name = "statix" version = "0.1.0" dependencies = [ - "anyhow", "ariadne", + "clap", + "globset", "lib", "rnix", + "thiserror", + "vfs", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.76" @@ -169,18 +365,118 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + [[package]] name = "text-size" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "288cb548dbe72b652243ea797201f3d481a0609a967980fcc5b2315ea811560a" +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "vfs" +version = "0.1.0" +dependencies = [ + "indexmap", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "yansi" version = "0.5.0" diff --git a/bin/Cargo.toml b/bin/Cargo.toml index 9452c2b..0d6f970 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -8,5 +8,8 @@ edition = "2018" [dependencies] lib = { path = "../lib" } ariadne = "0.1.3" -anyhow = "1.0" rnix = "0.9.0" +clap = "3.0.0-beta.4" +globset = "0.4.8" +thiserror = "1.0.30" +vfs = { path = "../vfs" } diff --git a/bin/src/config.rs b/bin/src/config.rs new file mode 100644 index 0000000..f2cf29d --- /dev/null +++ b/bin/src/config.rs @@ -0,0 +1,170 @@ +use std::{default::Default, fs, path::PathBuf, str::FromStr}; + +use clap::Clap; +use globset::{GlobBuilder, GlobSetBuilder}; +use vfs::ReadOnlyVfs; + +use crate::err::ConfigErr; + +/// Static analysis and linting for the nix programming language +#[derive(Clap, Debug)] +#[clap(version = "0.1.0", author = "Akshay ")] +pub struct Opts { + /// File or directory to run statix on + #[clap(default_value = ".")] + target: String, + + // /// Path to statix config + // #[clap(short, long, default_value = ".statix.toml")] + // config: String, + /// Regex of file patterns to not lint + #[clap(short, long)] + ignore: Vec, + + /// Output format. Supported values: json, errfmt + #[clap(short = 'o', long)] + format: Option, + + #[clap(subcommand)] + pub subcmd: Option, +} + +#[derive(Clap, Debug)] +#[clap(version = "0.1.0", author = "Akshay ")] +pub enum SubCommand { + /// Find and fix issues raised by statix + Fix(Fix), +} + +#[derive(Clap, Debug)] +pub struct Fix { + /// Do not write to files, display a diff instead + #[clap(short = 'd', long = "dry-run")] + diff_only: bool, +} + +#[derive(Debug, Copy, Clone)] +pub enum OutFormat { + Json, + Errfmt, + StdErr, +} + +impl Default for OutFormat { + fn default() -> Self { + OutFormat::StdErr + } +} + +impl FromStr for OutFormat { + type Err = &'static str; + + fn from_str(value: &str) -> Result { + match value.to_ascii_lowercase().as_str() { + "json" => Ok(Self::Json), + "errfmt" => Ok(Self::Errfmt), + "stderr" => Ok(Self::StdErr), + _ => Err("unknown output format, try: json, errfmt"), + } + } +} + +#[derive(Debug)] +pub struct LintConfig { + pub files: Vec, + pub format: OutFormat, +} + +impl LintConfig { + pub fn from_opts(opts: Opts) -> Result { + let ignores = { + let mut set = GlobSetBuilder::new(); + for pattern in opts.ignore { + let glob = GlobBuilder::new(&pattern).build().map_err(|err| { + ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) + })?; + set.add(glob); + } + set.build().map_err(|err| { + ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) + }) + }?; + + let walker = dirs::Walker::new(opts.target).map_err(ConfigErr::InvalidPath)?; + + let files = walker + .filter(|path| matches!(path.extension(), Some(e) if e == "nix")) + .filter(|path| !ignores.is_match(path)) + .collect(); + Ok(Self { + files, + format: opts.format.unwrap_or_default(), + }) + } + + pub fn vfs(&self) -> Result { + let mut vfs = ReadOnlyVfs::default(); + for file in self.files.iter() { + let _id = vfs.alloc_file_id(&file); + let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?; + vfs.set_file_contents(&file, data.as_bytes()); + } + Ok(vfs) + } +} + +mod dirs { + use std::{ + fs, + io::{self, Error, ErrorKind}, + path::{Path, PathBuf}, + }; + + #[derive(Default, Debug)] + pub struct Walker { + dirs: Vec, + files: Vec, + } + + impl Walker { + pub fn new>(target: P) -> io::Result { + let target = target.as_ref().to_path_buf(); + if !target.exists() { + Err(Error::new( + ErrorKind::NotFound, + format!("file not found: {}", target.display()), + )) + } else if target.is_dir() { + Ok(Self { + dirs: vec![target], + ..Default::default() + }) + } else { + Ok(Self { + files: vec![target], + ..Default::default() + }) + } + } + } + + impl Iterator for Walker { + type Item = PathBuf; + fn next(&mut self) -> Option { + if let Some(dir) = self.dirs.pop() { + if dir.is_dir() { + for entry in fs::read_dir(dir).ok()? { + let entry = entry.ok()?; + let path = entry.path(); + if path.is_dir() { + self.dirs.push(path); + } else if path.is_file() { + self.files.push(path); + } + } + } + } + self.files.pop() + } + } +} diff --git a/bin/src/err.rs b/bin/src/err.rs new file mode 100644 index 0000000..b3a79c2 --- /dev/null +++ b/bin/src/err.rs @@ -0,0 +1,28 @@ +use std::{io, path::PathBuf}; + +use globset::ErrorKind; +use rnix::parser::ParseError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ConfigErr { + #[error("error parsing glob `{0:?}`: {1}")] + InvalidGlob(Option, ErrorKind), + + #[error("path error: {0}")] + InvalidPath(#[from] io::Error), +} + +#[derive(Error, Debug)] +pub enum LintErr { + #[error("[{0}] syntax error: {1}")] + Parse(PathBuf, ParseError), +} + +#[derive(Error, Debug)] +pub enum StatixErr { + #[error("linter error: {0}")] + Lint(#[from] LintErr), + #[error("config error: {0}")] + Config(#[from] ConfigErr), +} diff --git a/bin/src/main.rs b/bin/src/main.rs index ab99aee..b26151d 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs @@ -1,20 +1,28 @@ -use std::{ - env, fs, - path::{Path, PathBuf}, -}; +#![feature(path_try_exists)] + +mod config; +mod err; +mod traits; -use anyhow::{Context, Result}; -use ariadne::{ - CharSet, Color, Config as CliConfig, Label, LabelAttach, Report as CliReport, - ReportKind as CliReportKind, Source, +use std::io; + +use crate::{ + err::{LintErr, StatixErr}, + traits::{LintResult, WriteDiagnostic}, }; -use lib::{Report, LINTS}; -use rnix::{TextRange, WalkEvent}; -fn analyze(source: &str) -> Result> { - let parsed = rnix::parse(source).as_result()?; +use clap::Clap; +use config::{LintConfig, Opts, SubCommand}; +use lib::LINTS; +use rnix::WalkEvent; +use vfs::VfsEntry; - Ok(parsed +fn analyze<'ρ>(vfs_entry: VfsEntry<'ρ>) -> Result { + let source = vfs_entry.contents; + let parsed = rnix::parse(source) + .as_result() + .map_err(|e| LintErr::Parse(vfs_entry.file_path.to_path_buf(), e))?; + let reports = parsed .node() .preorder_with_tokens() .filter_map(|event| match event { @@ -27,61 +35,33 @@ fn analyze(source: &str) -> Result> { _ => None, }) .flatten() - .collect()) + .collect(); + Ok(LintResult { + file_id: vfs_entry.file_id, + reports, + }) } -fn print_report(report: Report, file_src: &str, file_path: &Path) -> Result<()> { - let range = |at: TextRange| at.start().into()..at.end().into(); - let src_id = file_path.to_str().unwrap_or(""); - let offset = report - .diagnostics - .iter() - .map(|d| d.at.start().into()) - .min() - .unwrap_or(0usize); - report - .diagnostics - .iter() - .fold( - CliReport::build(CliReportKind::Warning, src_id, offset) - .with_config( - CliConfig::default() - .with_cross_gap(true) - .with_multiline_arrows(false) - .with_label_attach(LabelAttach::Middle) - .with_char_set(CharSet::Unicode), - ) - .with_message(report.note) - .with_code(report.code), - |cli_report, diagnostic| { - cli_report.with_label( - Label::new((src_id, range(diagnostic.at))) - .with_message(&diagnostic.message) - .with_color(Color::Magenta), - ) - }, - ) - .finish() - .eprint((src_id, Source::from(file_src))) - .context("failed to print report to stdout") -} - -fn _main() -> Result<()> { +fn _main() -> Result<(), StatixErr> { // TODO: accept cli args, construct a CLI config with a list of files to analyze - let args = env::args(); - for (file_src, file_path, reports) in args - .skip(1) - .map(|s| PathBuf::from(&s)) - .filter(|p| p.is_file()) - .filter_map(|path| { - let s = fs::read_to_string(&path).ok()?; - analyze(&s) - .map(|analysis_result| (s, path, analysis_result)) - .ok() - }) - { - for r in reports { - print_report(r, &file_src, &file_path)? + let opts = Opts::parse(); + match opts.subcmd { + Some(SubCommand::Fix(_)) => {} + None => { + let lint_config = LintConfig::from_opts(opts)?; + let vfs = lint_config.vfs()?; + let (reports, errors): (Vec<_>, Vec<_>) = + vfs.iter().map(analyze).partition(Result::is_ok); + let lint_results: Vec<_> = reports.into_iter().map(Result::unwrap).collect(); + let errors: Vec<_> = errors.into_iter().map(Result::unwrap_err).collect(); + + let mut stderr = io::stderr(); + lint_results.into_iter().for_each(|r| { + stderr.write(&r, &vfs).unwrap(); + }); + errors.into_iter().for_each(|e| { + eprintln!("{}", e); + }); } } Ok(()) @@ -90,6 +70,6 @@ fn _main() -> Result<()> { fn main() { match _main() { Err(e) => eprintln!("{}", e), - _ => {} + _ => (), } } diff --git a/bin/src/traits.rs b/bin/src/traits.rs new file mode 100644 index 0000000..1807ad0 --- /dev/null +++ b/bin/src/traits.rs @@ -0,0 +1,83 @@ +use std::{ + io::{self, Write}, + str, +}; + +use ariadne::{ + CharSet, Color, Config as CliConfig, Label, LabelAttach, Report as CliReport, + ReportKind as CliReportKind, Source, Fmt +}; +use lib::Report; +use rnix::TextRange; +use vfs::{FileId, ReadOnlyVfs}; + +#[derive(Debug)] +pub struct LintResult { + pub file_id: FileId, + pub reports: Vec, +} + +pub trait WriteDiagnostic { + fn write(&mut self, report: &LintResult, vfs: &ReadOnlyVfs) -> io::Result<()>; +} + +impl WriteDiagnostic for T +where + T: Write, +{ + fn write(&mut self, lint_result: &LintResult, vfs: &ReadOnlyVfs) -> io::Result<()> { + let file_id = lint_result.file_id; + let src = str::from_utf8(vfs.get(file_id)).unwrap(); + let path = vfs.file_path(file_id); + let range = |at: TextRange| at.start().into()..at.end().into(); + let src_id = path.to_str().unwrap_or(""); + for report in lint_result.reports.iter() { + let offset = report + .diagnostics + .iter() + .map(|d| d.at.start().into()) + .min() + .unwrap_or(0usize); + report + .diagnostics + .iter() + .fold( + CliReport::build(CliReportKind::Warning, src_id, offset) + .with_config( + CliConfig::default() + .with_cross_gap(true) + .with_multiline_arrows(false) + .with_label_attach(LabelAttach::Middle) + .with_char_set(CharSet::Unicode), + ) + .with_message(report.note) + .with_code(report.code), + |cli_report, diagnostic| { + cli_report.with_label( + Label::new((src_id, range(diagnostic.at))) + .with_message(&colorize(&diagnostic.message)) + .with_color(Color::Magenta), + ) + }, + ) + .finish() + .write((src_id, Source::from(src)), &mut *self)?; + } + Ok(()) + } +} + +// everything within backticks is colorized, backticks are removed +fn colorize(message: &str) -> String { + message.split('`') + .enumerate() + .map(|(idx, part)| { + if idx % 2 == 1 { + part.fg(Color::Cyan).to_string() + } else { + part.to_string() + } + }) + .collect::>() + .join("") +} -- cgit v1.2.3