aboutsummaryrefslogtreecommitdiff
path: root/bin/src
diff options
context:
space:
mode:
authorAkshay <[email protected]>2021-10-25 17:38:52 +0100
committerAkshay <[email protected]>2021-10-25 17:38:52 +0100
commit5f0a1e67c64082c848418daa2b51020eb42c5c12 (patch)
tree1d6d7db9ee5532e76d23b9f509a8299d0d34dc52 /bin/src
parent781c42cc9ce2e6a3f1024ea1f4e3f071cc8f2dd4 (diff)
rework cli, use subcommands instead
Diffstat (limited to 'bin/src')
-rw-r--r--bin/src/config.rs213
-rw-r--r--bin/src/err.rs17
-rw-r--r--bin/src/fix.rs87
-rw-r--r--bin/src/fix/all.rs86
-rw-r--r--bin/src/fix/single.rs59
-rw-r--r--bin/src/main.rs72
6 files changed, 326 insertions, 208 deletions
diff --git a/bin/src/config.rs b/bin/src/config.rs
index 6d7bc49..202ff6c 100644
--- a/bin/src/config.rs
+++ b/bin/src/config.rs
@@ -15,118 +15,74 @@ use crate::err::ConfigErr;
15#[derive(Clap, Debug)] 15#[derive(Clap, Debug)]
16#[clap(version = "0.1.0", author = "Akshay <[email protected]>")] 16#[clap(version = "0.1.0", author = "Akshay <[email protected]>")]
17pub struct Opts { 17pub struct Opts {
18 /// File or directory to run statix on 18 #[clap(subcommand)]
19 #[clap(default_value = ".")] 19 pub cmd: SubCommand,
20 pub target: String, 20}
21
22#[derive(Clap, Debug)]
23pub enum SubCommand {
24 /// Lints and suggestions for the nix programming language
25 Check(Check),
26 /// Find and fix issues raised by statix-check
27 Fix(Fix),
28 /// Fix exactly one issue at provided position
29 Single(Single),
30}
31
32#[derive(Clap, Debug)]
33pub struct Check {
34 /// File or directory to run check on
35 #[clap(default_value = ".", parse(from_os_str))]
36 target: PathBuf,
21 37
22 /// Globs of file patterns to skip 38 /// Globs of file patterns to skip
23 #[clap(short, long)] 39 #[clap(short, long)]
24 pub ignore: Vec<String>, 40 ignore: Vec<String>,
25 41
26 /// Output format. 42 /// Output format.
27 /// Supported values: errfmt, json (on feature flag only) 43 /// Supported values: errfmt, json (on feature flag only)
28 #[clap(short = 'o', long)] 44 #[clap(short = 'o', long, default_value = "OutFormat::StdErr")]
29 format: Option<OutFormat>, 45 pub format: OutFormat,
30
31 /// Find and fix issues raised by statix
32 #[clap(short = 'f', long)]
33 pub fix: bool,
34
35 /// Do not fix files in place, display a diff instead
36 #[clap(short = 'd', long = "dry-run")]
37 diff_only: bool,
38}
39
40
41#[derive(Debug, Copy, Clone)]
42pub enum OutFormat {
43 #[cfg(feature = "json")]
44 Json,
45 Errfmt,
46 StdErr,
47} 46}
48 47
49impl Default for OutFormat { 48impl Check {
50 fn default() -> Self { 49 pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> {
51 OutFormat::StdErr 50 let files = walk_with_ignores(&self.ignore, &self.target)?;
51 vfs(files)
52 } 52 }
53} 53}
54 54
55impl FromStr for OutFormat { 55#[derive(Clap, Debug)]
56 type Err = &'static str; 56pub struct Fix {
57 /// File or directory to run fix on
58 #[clap(default_value = ".", parse(from_os_str))]
59 target: PathBuf,
57 60
58 fn from_str(value: &str) -> Result<Self, Self::Err> { 61 /// Globs of file patterns to skip
59 match value.to_ascii_lowercase().as_str() { 62 #[clap(short, long)]
60 #[cfg(feature = "json")] "json" => Ok(Self::Json), 63 ignore: Vec<String>,
61 "errfmt" => Ok(Self::Errfmt),
62 "stderr" => Ok(Self::StdErr),
63 "json" => Err("statix was not compiled with the `json` feature flag"),
64 _ => Err("unknown output format, try: json, errfmt"),
65 }
66 }
67}
68 64
69#[derive(Debug)] 65 /// Do not fix files in place, display a diff instead
70pub struct LintConfig { 66 #[clap(short, long = "dry-run")]
71 pub files: Vec<PathBuf>, 67 pub diff_only: bool,
72 pub format: OutFormat,
73} 68}
74 69
75impl LintConfig { 70impl Fix {
76 pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> {
77 let ignores = build_ignore_set(&opts.ignore).map_err(|err| {
78 ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone())
79 })?;
80
81 let files = walk_nix_files(&opts.target)?
82 .filter(|path| !ignores.is_match(path))
83 .collect();
84
85 Ok(Self {
86 files,
87 format: opts.format.unwrap_or_default(),
88 })
89 }
90
91 pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> { 71 pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> {
92 let mut vfs = ReadOnlyVfs::default(); 72 let files = walk_with_ignores(&self.ignore, &self.target)?;
93 for file in self.files.iter() { 73 vfs(files)
94 let _id = vfs.alloc_file_id(&file);
95 let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?;
96 vfs.set_file_contents(&file, data.as_bytes());
97 }
98 Ok(vfs)
99 } 74 }
100} 75}
101 76
102pub struct FixConfig { 77#[derive(Clap, Debug)]
103 pub files: Vec<PathBuf>, 78pub struct Single {
104 pub diff_only: bool, 79 /// File to run single-fix on
105} 80 #[clap(default_value = ".", parse(from_os_str))]
106 81 pub target: PathBuf,
107impl FixConfig { 82
108 pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> { 83 /// Position to attempt a fix at
109 let ignores = build_ignore_set(&opts.ignore).map_err(|err| { 84 #[clap(short, long, parse(try_from_str = parse_line_col))]
110 ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) 85 pub position: (usize, usize),
111 })?;
112
113 let files = walk_nix_files(&opts.target)?
114 .filter(|path| !ignores.is_match(path))
115 .collect();
116
117 let diff_only = opts.diff_only;
118 Ok(Self { files, diff_only })
119 }
120
121 pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> {
122 let mut vfs = ReadOnlyVfs::default();
123 for file in self.files.iter() {
124 let _id = vfs.alloc_file_id(&file);
125 let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?;
126 vfs.set_file_contents(&file, data.as_bytes());
127 }
128 Ok(vfs)
129 }
130} 86}
131 87
132mod dirs { 88mod dirs {
@@ -185,7 +141,23 @@ mod dirs {
185 } 141 }
186} 142}
187 143
188fn build_ignore_set(ignores: &Vec<String>) -> Result<GlobSet, GlobError> { 144fn parse_line_col(src: &str) -> Result<(usize, usize), ConfigErr> {
145 let parts = src.split(",");
146 match parts.collect::<Vec<_>>().as_slice() {
147 [line, col] => {
148 let l = line
149 .parse::<usize>()
150 .map_err(|_| ConfigErr::InvalidPosition(src.to_owned()))?;
151 let c = col
152 .parse::<usize>()
153 .map_err(|_| ConfigErr::InvalidPosition(src.to_owned()))?;
154 Ok((l, c))
155 }
156 _ => Err(ConfigErr::InvalidPosition(src.to_owned())),
157 }
158}
159
160fn build_ignore_set(ignores: &[String]) -> Result<GlobSet, GlobError> {
189 let mut set = GlobSetBuilder::new(); 161 let mut set = GlobSetBuilder::new();
190 for pattern in ignores { 162 for pattern in ignores {
191 let glob = GlobBuilder::new(&pattern).build()?; 163 let glob = GlobBuilder::new(&pattern).build()?;
@@ -198,3 +170,56 @@ fn walk_nix_files<P: AsRef<Path>>(target: P) -> Result<impl Iterator<Item = Path
198 let walker = dirs::Walker::new(target)?; 170 let walker = dirs::Walker::new(target)?;
199 Ok(walker.filter(|path: &PathBuf| matches!(path.extension(), Some(e) if e == "nix"))) 171 Ok(walker.filter(|path: &PathBuf| matches!(path.extension(), Some(e) if e == "nix")))
200} 172}
173
174fn walk_with_ignores<P: AsRef<Path>>(
175 ignores: &[String],
176 target: P,
177) -> Result<Vec<PathBuf>, ConfigErr> {
178 let ignores = build_ignore_set(ignores).map_err(|err| {
179 ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone())
180 })?;
181
182 Ok(walk_nix_files(&target)?
183 .filter(|path| !ignores.is_match(path))
184 .collect())
185}
186
187fn vfs(files: Vec<PathBuf>) -> Result<ReadOnlyVfs, ConfigErr> {
188 let mut vfs = ReadOnlyVfs::default();
189 for file in files.iter() {
190 let _id = vfs.alloc_file_id(&file);
191 let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?;
192 vfs.set_file_contents(&file, data.as_bytes());
193 }
194 Ok(vfs)
195
196}
197
198#[derive(Debug, Copy, Clone)]
199pub enum OutFormat {
200 #[cfg(feature = "json")]
201 Json,
202 Errfmt,
203 StdErr,
204}
205
206impl Default for OutFormat {
207 fn default() -> Self {
208 OutFormat::StdErr
209 }
210}
211
212impl FromStr for OutFormat {
213 type Err = &'static str;
214
215 fn from_str(value: &str) -> Result<Self, Self::Err> {
216 match value.to_ascii_lowercase().as_str() {
217 #[cfg(feature = "json")]
218 "json" => Ok(Self::Json),
219 "errfmt" => Ok(Self::Errfmt),
220 "stderr" => Ok(Self::StdErr),
221 "json" => Err("statix was not compiled with the `json` feature flag"),
222 _ => Err("unknown output format, try: json, errfmt"),
223 }
224 }
225}
diff --git a/bin/src/err.rs b/bin/src/err.rs
index 727e0cc..5f71b57 100644
--- a/bin/src/err.rs
+++ b/bin/src/err.rs
@@ -8,9 +8,10 @@ use thiserror::Error;
8pub enum ConfigErr { 8pub enum ConfigErr {
9 #[error("error parsing glob `{0:?}`: {1}")] 9 #[error("error parsing glob `{0:?}`: {1}")]
10 InvalidGlob(Option<String>, ErrorKind), 10 InvalidGlob(Option<String>, ErrorKind),
11
12 #[error("path error: {0}")] 11 #[error("path error: {0}")]
13 InvalidPath(#[from] io::Error), 12 InvalidPath(#[from] io::Error),
13 #[error("unable to parse `{0}` as line and column")]
14 InvalidPosition(String)
14} 15}
15 16
16#[derive(Error, Debug)] 17#[derive(Error, Debug)]
@@ -28,11 +29,25 @@ pub enum FixErr {
28} 29}
29 30
30#[derive(Error, Debug)] 31#[derive(Error, Debug)]
32pub enum SingleFixErr {
33 #[error("path error: {0}")]
34 InvalidPath(#[from] io::Error),
35 #[error("position out of bounds: line {0}, col {1}")]
36 OutOfBounds(usize, usize),
37 #[error("{0} is too large")]
38 Conversion(usize),
39 #[error("nothing to fix")]
40 NoOp,
41}
42
43#[derive(Error, Debug)]
31pub enum StatixErr { 44pub enum StatixErr {
32 #[error("linter error: {0}")] 45 #[error("linter error: {0}")]
33 Lint(#[from] LintErr), 46 Lint(#[from] LintErr),
34 #[error("fixer error: {0}")] 47 #[error("fixer error: {0}")]
35 Fix(#[from] FixErr), 48 Fix(#[from] FixErr),
49 #[error("single fix error: {0}")]
50 Single(#[from] SingleFixErr),
36 #[error("config error: {0}")] 51 #[error("config error: {0}")]
37 Config(#[from] ConfigErr), 52 Config(#[from] ConfigErr),
38} 53}
diff --git a/bin/src/fix.rs b/bin/src/fix.rs
index d9087fe..a7ddc4f 100644
--- a/bin/src/fix.rs
+++ b/bin/src/fix.rs
@@ -1,55 +1,14 @@
1use std::borrow::Cow; 1use std::borrow::Cow;
2 2
3use lib::{Report, LINTS}; 3use rnix::TextRange;
4use rnix::{parser::ParseError as RnixParseErr, TextRange, WalkEvent};
5 4
6type Source<'a> = Cow<'a, str>; 5mod all;
7 6pub use all::all;
8fn collect_fixes(source: &str) -> Result<Vec<Report>, RnixParseErr> {
9 let parsed = rnix::parse(source).as_result()?;
10
11 Ok(parsed
12 .node()
13 .preorder_with_tokens()
14 .filter_map(|event| match event {
15 WalkEvent::Enter(child) => LINTS.get(&child.kind()).map(|rules| {
16 rules
17 .iter()
18 .filter_map(|rule| rule.validate(&child))
19 .filter(|report| report.total_suggestion_range().is_some())
20 .collect::<Vec<_>>()
21 }),
22 _ => None,
23 })
24 .flatten()
25 .collect())
26}
27 7
28fn reorder(mut reports: Vec<Report>) -> Vec<Report> { 8mod single;
29 use std::collections::VecDeque; 9pub use single::single;
30 10
31 reports.sort_by(|a, b| { 11type Source<'a> = Cow<'a, str>;
32 let a_range = a.range();
33 let b_range = b.range();
34 a_range.end().partial_cmp(&b_range.end()).unwrap()
35 });
36
37 reports
38 .into_iter()
39 .fold(VecDeque::new(), |mut deque: VecDeque<Report>, new_elem| {
40 let front = deque.front();
41 let new_range = new_elem.range();
42 if let Some(front_range) = front.map(|f| f.range()) {
43 if new_range.start() > front_range.end() {
44 deque.push_front(new_elem);
45 }
46 } else {
47 deque.push_front(new_elem);
48 }
49 deque
50 })
51 .into()
52}
53 12
54#[derive(Debug)] 13#[derive(Debug)]
55pub struct FixResult<'a> { 14pub struct FixResult<'a> {
@@ -68,37 +27,3 @@ impl<'a> FixResult<'a> {
68 Self { src, fixed: Vec::new() } 27 Self { src, fixed: Vec::new() }
69 } 28 }
70} 29}
71
72impl<'a> Iterator for FixResult<'a> {
73 type Item = FixResult<'a>;
74 fn next(&mut self) -> Option<Self::Item> {
75 let all_reports = collect_fixes(&self.src).ok()?;
76 if all_reports.is_empty() {
77 return None;
78 }
79
80 let reordered = reorder(all_reports);
81 let fixed = reordered
82 .iter()
83 .map(|r| Fixed {
84 at: r.range(),
85 code: r.code,
86 })
87 .collect::<Vec<_>>();
88 for report in reordered {
89 report.apply(self.src.to_mut());
90 }
91
92 Some(FixResult {
93 src: self.src.clone(),
94 fixed
95 })
96 }
97}
98
99pub fn fix(src: &str) -> Option<FixResult> {
100 let src = Cow::from(src);
101 let _ = rnix::parse(&src).as_result().ok()?;
102 let initial = FixResult::empty(src);
103 initial.into_iter().last()
104}
diff --git a/bin/src/fix/all.rs b/bin/src/fix/all.rs
new file mode 100644
index 0000000..8c0770d
--- /dev/null
+++ b/bin/src/fix/all.rs
@@ -0,0 +1,86 @@
1use std::borrow::Cow;
2
3use lib::{Report, LINTS};
4use rnix::{parser::ParseError as RnixParseErr, WalkEvent};
5
6use crate::fix::{Fixed, FixResult};
7
8fn collect_fixes(source: &str) -> Result<Vec<Report>, RnixParseErr> {
9 let parsed = rnix::parse(source).as_result()?;
10
11 Ok(parsed
12 .node()
13 .preorder_with_tokens()
14 .filter_map(|event| match event {
15 WalkEvent::Enter(child) => LINTS.get(&child.kind()).map(|rules| {
16 rules
17 .iter()
18 .filter_map(|rule| rule.validate(&child))
19 .filter(|report| report.total_suggestion_range().is_some())
20 .collect::<Vec<_>>()
21 }),
22 _ => None,
23 })
24 .flatten()
25 .collect())
26}
27
28fn reorder(mut reports: Vec<Report>) -> Vec<Report> {
29 use std::collections::VecDeque;
30
31 reports.sort_by(|a, b| {
32 let a_range = a.range();
33 let b_range = b.range();
34 a_range.end().partial_cmp(&b_range.end()).unwrap()
35 });
36
37 reports
38 .into_iter()
39 .fold(VecDeque::new(), |mut deque: VecDeque<Report>, new_elem| {
40 let front = deque.front();
41 let new_range = new_elem.range();
42 if let Some(front_range) = front.map(|f| f.range()) {
43 if new_range.start() > front_range.end() {
44 deque.push_front(new_elem);
45 }
46 } else {
47 deque.push_front(new_elem);
48 }
49 deque
50 })
51 .into()
52}
53
54impl<'a> Iterator for FixResult<'a> {
55 type Item = FixResult<'a>;
56 fn next(&mut self) -> Option<Self::Item> {
57 let all_reports = collect_fixes(&self.src).ok()?;
58 if all_reports.is_empty() {
59 return None;
60 }
61
62 let reordered = reorder(all_reports);
63 let fixed = reordered
64 .iter()
65 .map(|r| Fixed {
66 at: r.range(),
67 code: r.code,
68 })
69 .collect::<Vec<_>>();
70 for report in reordered {
71 report.apply(self.src.to_mut());
72 }
73
74 Some(FixResult {
75 src: self.src.clone(),
76 fixed
77 })
78 }
79}
80
81pub fn all(src: &str) -> Option<FixResult> {
82 let src = Cow::from(src);
83 let _ = rnix::parse(&src).as_result().ok()?;
84 let initial = FixResult::empty(src);
85 initial.into_iter().last()
86}
diff --git a/bin/src/fix/single.rs b/bin/src/fix/single.rs
new file mode 100644
index 0000000..d430693
--- /dev/null
+++ b/bin/src/fix/single.rs
@@ -0,0 +1,59 @@
1use std::{borrow::Cow, convert::TryFrom};
2
3use lib::{Report, LINTS};
4use rnix::{TextRange, TextSize};
5
6use crate::err::SingleFixErr;
7use crate::fix::Source;
8
9pub struct SingleFixResult<'δ> {
10 pub src: Source<'δ>,
11}
12
13fn pos_to_byte(line: usize, col: usize, src: &str) -> Result<TextSize, SingleFixErr> {
14 let mut byte: TextSize = TextSize::of("");
15 for (_, l) in src.lines().enumerate().take_while(|(i, _)| i <= &line) {
16 byte += TextSize::of(l);
17 }
18 byte += TextSize::try_from(col).map_err(|_| SingleFixErr::Conversion(col))?;
19
20 if usize::from(byte) >= src.len() {
21 Err(SingleFixErr::OutOfBounds(line, col))
22 } else {
23 Ok(byte)
24 }
25}
26
27fn find(offset: TextSize, src: &str) -> Result<Report, SingleFixErr> {
28 // we don't really need the source to form a completely parsed tree
29 let parsed = rnix::parse(src);
30
31 let elem_at = parsed
32 .node()
33 .child_or_token_at_range(TextRange::empty(offset))
34 .ok_or(SingleFixErr::NoOp)?;
35
36 LINTS
37 .get(&elem_at.kind())
38 .map(|rules| {
39 rules
40 .iter()
41 .filter_map(|rule| rule.validate(&elem_at))
42 .filter(|report| report.total_suggestion_range().is_some())
43 .next()
44 })
45 .flatten()
46 .ok_or(SingleFixErr::NoOp)
47}
48
49pub fn single(line: usize, col: usize, src: &str) -> Result<SingleFixResult, SingleFixErr> {
50 let mut src = Cow::from(src);
51 let offset = pos_to_byte(line, col, &*src)?;
52 let report = find(offset, &*src)?;
53
54 report.apply(src.to_mut());
55
56 Ok(SingleFixResult {
57 src
58 })
59}
diff --git a/bin/src/main.rs b/bin/src/main.rs
index d0f69a0..9c57d91 100644
--- a/bin/src/main.rs
+++ b/bin/src/main.rs
@@ -6,50 +6,58 @@ mod traits;
6 6
7use std::io; 7use std::io;
8 8
9use crate::{err::{StatixErr, FixErr}, traits::WriteDiagnostic}; 9use crate::{err::{StatixErr, FixErr, SingleFixErr}, traits::WriteDiagnostic};
10 10
11use clap::Clap; 11use clap::Clap;
12use config::{FixConfig, LintConfig, Opts}; 12use config::{Opts, SubCommand};
13use similar::TextDiff; 13use similar::TextDiff;
14 14
15fn _main() -> Result<(), StatixErr> { 15fn _main() -> Result<(), StatixErr> {
16 let opts = Opts::parse(); 16 let opts = Opts::parse();
17 if opts.fix { 17 match opts.cmd {
18 let fix_config = FixConfig::from_opts(opts)?; 18 SubCommand::Check(check_config) => {
19 let vfs = fix_config.vfs()?; 19 let vfs = check_config.vfs()?;
20 for entry in vfs.iter() { 20 let (lints, errors): (Vec<_>, Vec<_>) = vfs.iter().map(lint::lint).partition(Result::is_ok);
21 if let Some(fix_result) = fix::fix(entry.contents) { 21 let lint_results = lints.into_iter().map(Result::unwrap);
22 if fix_config.diff_only { 22 let errors = errors.into_iter().map(Result::unwrap_err);
23 let text_diff = TextDiff::from_lines(entry.contents, &fix_result.src); 23
24 let old_file = format!("{}", entry.file_path.display()); 24 let mut stdout = io::stdout();
25 let new_file = format!("{} [fixed]", entry.file_path.display()); 25 lint_results.for_each(|r| {
26 println!( 26 stdout.write(&r, &vfs, check_config.format).unwrap();
27 "{}", 27 });
28 text_diff 28 errors.for_each(|e| {
29 eprintln!("{}", e);
30 });
31 },
32 SubCommand::Fix(fix_config) => {
33 let vfs = fix_config.vfs()?;
34 for entry in vfs.iter() {
35 if let Some(fix_result) = fix::all(entry.contents) {
36 if fix_config.diff_only {
37 let text_diff = TextDiff::from_lines(entry.contents, &fix_result.src);
38 let old_file = format!("{}", entry.file_path.display());
39 let new_file = format!("{} [fixed]", entry.file_path.display());
40 println!(
41 "{}",
42 text_diff
29 .unified_diff() 43 .unified_diff()
30 .context_radius(4) 44 .context_radius(4)
31 .header(&old_file, &new_file) 45 .header(&old_file, &new_file)
32 ); 46 );
33 } else { 47 } else {
34 let path = entry.file_path; 48 let path = entry.file_path;
35 std::fs::write(path, &*fix_result.src).map_err(FixErr::InvalidPath)?; 49 std::fs::write(path, &*fix_result.src).map_err(FixErr::InvalidPath)?;
50 }
36 } 51 }
37 } 52 }
53 },
54 SubCommand::Single(single_config) => {
55 let path = single_config.target;
56 let src = std::fs::read_to_string(&path).map_err(SingleFixErr::InvalidPath)?;
57 let (line, col) = single_config.position;
58 let single_result = fix::single(line, col, &src)?;
59 std::fs::write(&path, &*single_result.src).map_err(SingleFixErr::InvalidPath)?;
38 } 60 }
39 } else {
40 let lint_config = LintConfig::from_opts(opts)?;
41 let vfs = lint_config.vfs()?;
42 let (lints, errors): (Vec<_>, Vec<_>) = vfs.iter().map(lint::lint).partition(Result::is_ok);
43 let lint_results = lints.into_iter().map(Result::unwrap);
44 let errors = errors.into_iter().map(Result::unwrap_err);
45
46 let mut stdout = io::stdout();
47 lint_results.for_each(|r| {
48 stdout.write(&r, &vfs, lint_config.format).unwrap();
49 });
50 errors.for_each(|e| {
51 eprintln!("{}", e);
52 });
53 } 61 }
54 Ok(()) 62 Ok(())
55} 63}