aboutsummaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to 'bin')
-rw-r--r--bin/Cargo.toml5
-rw-r--r--bin/src/config.rs170
-rw-r--r--bin/src/err.rs28
-rw-r--r--bin/src/main.rs112
-rw-r--r--bin/src/traits.rs83
5 files changed, 331 insertions, 67 deletions
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"
8[dependencies] 8[dependencies]
9lib = { path = "../lib" } 9lib = { path = "../lib" }
10ariadne = "0.1.3" 10ariadne = "0.1.3"
11anyhow = "1.0"
12rnix = "0.9.0" 11rnix = "0.9.0"
12clap = "3.0.0-beta.4"
13globset = "0.4.8"
14thiserror = "1.0.30"
15vfs = { 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 @@
1use std::{default::Default, fs, path::PathBuf, str::FromStr};
2
3use clap::Clap;
4use globset::{GlobBuilder, GlobSetBuilder};
5use vfs::ReadOnlyVfs;
6
7use crate::err::ConfigErr;
8
9/// Static analysis and linting for the nix programming language
10#[derive(Clap, Debug)]
11#[clap(version = "0.1.0", author = "Akshay <[email protected]>")]
12pub struct Opts {
13 /// File or directory to run statix on
14 #[clap(default_value = ".")]
15 target: String,
16
17 // /// Path to statix config
18 // #[clap(short, long, default_value = ".statix.toml")]
19 // config: String,
20 /// Regex of file patterns to not lint
21 #[clap(short, long)]
22 ignore: Vec<String>,
23
24 /// Output format. Supported values: json, errfmt
25 #[clap(short = 'o', long)]
26 format: Option<OutFormat>,
27
28 #[clap(subcommand)]
29 pub subcmd: Option<SubCommand>,
30}
31
32#[derive(Clap, Debug)]
33#[clap(version = "0.1.0", author = "Akshay <[email protected]>")]
34pub enum SubCommand {
35 /// Find and fix issues raised by statix
36 Fix(Fix),
37}
38
39#[derive(Clap, Debug)]
40pub struct Fix {
41 /// Do not write to files, display a diff instead
42 #[clap(short = 'd', long = "dry-run")]
43 diff_only: bool,
44}
45
46#[derive(Debug, Copy, Clone)]
47pub enum OutFormat {
48 Json,
49 Errfmt,
50 StdErr,
51}
52
53impl Default for OutFormat {
54 fn default() -> Self {
55 OutFormat::StdErr
56 }
57}
58
59impl FromStr for OutFormat {
60 type Err = &'static str;
61
62 fn from_str(value: &str) -> Result<Self, Self::Err> {
63 match value.to_ascii_lowercase().as_str() {
64 "json" => Ok(Self::Json),
65 "errfmt" => Ok(Self::Errfmt),
66 "stderr" => Ok(Self::StdErr),
67 _ => Err("unknown output format, try: json, errfmt"),
68 }
69 }
70}
71
72#[derive(Debug)]
73pub struct LintConfig {
74 pub files: Vec<PathBuf>,
75 pub format: OutFormat,
76}
77
78impl LintConfig {
79 pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> {
80 let ignores = {
81 let mut set = GlobSetBuilder::new();
82 for pattern in opts.ignore {
83 let glob = GlobBuilder::new(&pattern).build().map_err(|err| {
84 ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone())
85 })?;
86 set.add(glob);
87 }
88 set.build().map_err(|err| {
89 ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone())
90 })
91 }?;
92
93 let walker = dirs::Walker::new(opts.target).map_err(ConfigErr::InvalidPath)?;
94
95 let files = walker
96 .filter(|path| matches!(path.extension(), Some(e) if e == "nix"))
97 .filter(|path| !ignores.is_match(path))
98 .collect();
99 Ok(Self {
100 files,
101 format: opts.format.unwrap_or_default(),
102 })
103 }
104
105 pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> {
106 let mut vfs = ReadOnlyVfs::default();
107 for file in self.files.iter() {
108 let _id = vfs.alloc_file_id(&file);
109 let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?;
110 vfs.set_file_contents(&file, data.as_bytes());
111 }
112 Ok(vfs)
113 }
114}
115
116mod dirs {
117 use std::{
118 fs,
119 io::{self, Error, ErrorKind},
120 path::{Path, PathBuf},
121 };
122
123 #[derive(Default, Debug)]
124 pub struct Walker {
125 dirs: Vec<PathBuf>,
126 files: Vec<PathBuf>,
127 }
128
129 impl Walker {
130 pub fn new<P: AsRef<Path>>(target: P) -> io::Result<Self> {
131 let target = target.as_ref().to_path_buf();
132 if !target.exists() {
133 Err(Error::new(
134 ErrorKind::NotFound,
135 format!("file not found: {}", target.display()),
136 ))
137 } else if target.is_dir() {
138 Ok(Self {
139 dirs: vec![target],
140 ..Default::default()
141 })
142 } else {
143 Ok(Self {
144 files: vec![target],
145 ..Default::default()
146 })
147 }
148 }
149 }
150
151 impl Iterator for Walker {
152 type Item = PathBuf;
153 fn next(&mut self) -> Option<Self::Item> {
154 if let Some(dir) = self.dirs.pop() {
155 if dir.is_dir() {
156 for entry in fs::read_dir(dir).ok()? {
157 let entry = entry.ok()?;
158 let path = entry.path();
159 if path.is_dir() {
160 self.dirs.push(path);
161 } else if path.is_file() {
162 self.files.push(path);
163 }
164 }
165 }
166 }
167 self.files.pop()
168 }
169 }
170}
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 @@
1use std::{io, path::PathBuf};
2
3use globset::ErrorKind;
4use rnix::parser::ParseError;
5use thiserror::Error;
6
7#[derive(Error, Debug)]
8pub enum ConfigErr {
9 #[error("error parsing glob `{0:?}`: {1}")]
10 InvalidGlob(Option<String>, ErrorKind),
11
12 #[error("path error: {0}")]
13 InvalidPath(#[from] io::Error),
14}
15
16#[derive(Error, Debug)]
17pub enum LintErr {
18 #[error("[{0}] syntax error: {1}")]
19 Parse(PathBuf, ParseError),
20}
21
22#[derive(Error, Debug)]
23pub enum StatixErr {
24 #[error("linter error: {0}")]
25 Lint(#[from] LintErr),
26 #[error("config error: {0}")]
27 Config(#[from] ConfigErr),
28}
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 @@
1use std::{ 1#![feature(path_try_exists)]
2 env, fs, 2
3 path::{Path, PathBuf}, 3mod config;
4}; 4mod err;
5mod traits;
5 6
6use anyhow::{Context, Result}; 7use std::io;
7use ariadne::{ 8
8 CharSet, Color, Config as CliConfig, Label, LabelAttach, Report as CliReport, 9use crate::{
9 ReportKind as CliReportKind, Source, 10 err::{LintErr, StatixErr},
11 traits::{LintResult, WriteDiagnostic},
10}; 12};
11use lib::{Report, LINTS};
12use rnix::{TextRange, WalkEvent};
13 13
14fn analyze(source: &str) -> Result<Vec<Report>> { 14use clap::Clap;
15 let parsed = rnix::parse(source).as_result()?; 15use config::{LintConfig, Opts, SubCommand};
16use lib::LINTS;
17use rnix::WalkEvent;
18use vfs::VfsEntry;
16 19
17 Ok(parsed 20fn analyze<'ρ>(vfs_entry: VfsEntry<'ρ>) -> Result<LintResult, LintErr> {
21 let source = vfs_entry.contents;
22 let parsed = rnix::parse(source)
23 .as_result()
24 .map_err(|e| LintErr::Parse(vfs_entry.file_path.to_path_buf(), e))?;
25 let reports = parsed
18 .node() 26 .node()
19 .preorder_with_tokens() 27 .preorder_with_tokens()
20 .filter_map(|event| match event { 28 .filter_map(|event| match event {
@@ -27,61 +35,33 @@ fn analyze(source: &str) -> Result<Vec<Report>> {
27 _ => None, 35 _ => None,
28 }) 36 })
29 .flatten() 37 .flatten()
30 .collect()) 38 .collect();
39 Ok(LintResult {
40 file_id: vfs_entry.file_id,
41 reports,
42 })
31} 43}
32 44
33fn print_report(report: Report, file_src: &str, file_path: &Path) -> Result<()> { 45fn _main() -> Result<(), StatixErr> {
34 let range = |at: TextRange| at.start().into()..at.end().into();
35 let src_id = file_path.to_str().unwrap_or("<unknown>");
36 let offset = report
37 .diagnostics
38 .iter()
39 .map(|d| d.at.start().into())
40 .min()
41 .unwrap_or(0usize);
42 report
43 .diagnostics
44 .iter()
45 .fold(
46 CliReport::build(CliReportKind::Warning, src_id, offset)
47 .with_config(
48 CliConfig::default()
49 .with_cross_gap(true)
50 .with_multiline_arrows(false)
51 .with_label_attach(LabelAttach::Middle)
52 .with_char_set(CharSet::Unicode),
53 )
54 .with_message(report.note)
55 .with_code(report.code),
56 |cli_report, diagnostic| {
57 cli_report.with_label(
58 Label::new((src_id, range(diagnostic.at)))
59 .with_message(&diagnostic.message)
60 .with_color(Color::Magenta),
61 )
62 },
63 )
64 .finish()
65 .eprint((src_id, Source::from(file_src)))
66 .context("failed to print report to stdout")
67}
68
69fn _main() -> Result<()> {
70 // TODO: accept cli args, construct a CLI config with a list of files to analyze 46 // TODO: accept cli args, construct a CLI config with a list of files to analyze
71 let args = env::args(); 47 let opts = Opts::parse();
72 for (file_src, file_path, reports) in args 48 match opts.subcmd {
73 .skip(1) 49 Some(SubCommand::Fix(_)) => {}
74 .map(|s| PathBuf::from(&s)) 50 None => {
75 .filter(|p| p.is_file()) 51 let lint_config = LintConfig::from_opts(opts)?;
76 .filter_map(|path| { 52 let vfs = lint_config.vfs()?;
77 let s = fs::read_to_string(&path).ok()?; 53 let (reports, errors): (Vec<_>, Vec<_>) =
78 analyze(&s) 54 vfs.iter().map(analyze).partition(Result::is_ok);
79 .map(|analysis_result| (s, path, analysis_result)) 55 let lint_results: Vec<_> = reports.into_iter().map(Result::unwrap).collect();
80 .ok() 56 let errors: Vec<_> = errors.into_iter().map(Result::unwrap_err).collect();
81 }) 57
82 { 58 let mut stderr = io::stderr();
83 for r in reports { 59 lint_results.into_iter().for_each(|r| {
84 print_report(r, &file_src, &file_path)? 60 stderr.write(&r, &vfs).unwrap();
61 });
62 errors.into_iter().for_each(|e| {
63 eprintln!("{}", e);
64 });
85 } 65 }
86 } 66 }
87 Ok(()) 67 Ok(())
@@ -90,6 +70,6 @@ fn _main() -> Result<()> {
90fn main() { 70fn main() {
91 match _main() { 71 match _main() {
92 Err(e) => eprintln!("{}", e), 72 Err(e) => eprintln!("{}", e),
93 _ => {} 73 _ => (),
94 } 74 }
95} 75}
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 @@
1use std::{
2 io::{self, Write},
3 str,
4};
5
6use ariadne::{
7 CharSet, Color, Config as CliConfig, Label, LabelAttach, Report as CliReport,
8 ReportKind as CliReportKind, Source, Fmt
9};
10use lib::Report;
11use rnix::TextRange;
12use vfs::{FileId, ReadOnlyVfs};
13
14#[derive(Debug)]
15pub struct LintResult {
16 pub file_id: FileId,
17 pub reports: Vec<Report>,
18}
19
20pub trait WriteDiagnostic {
21 fn write(&mut self, report: &LintResult, vfs: &ReadOnlyVfs) -> io::Result<()>;
22}
23
24impl<T> WriteDiagnostic for T
25where
26 T: Write,
27{
28 fn write(&mut self, lint_result: &LintResult, vfs: &ReadOnlyVfs) -> io::Result<()> {
29 let file_id = lint_result.file_id;
30 let src = str::from_utf8(vfs.get(file_id)).unwrap();
31 let path = vfs.file_path(file_id);
32 let range = |at: TextRange| at.start().into()..at.end().into();
33 let src_id = path.to_str().unwrap_or("<unknown>");
34 for report in lint_result.reports.iter() {
35 let offset = report
36 .diagnostics
37 .iter()
38 .map(|d| d.at.start().into())
39 .min()
40 .unwrap_or(0usize);
41 report
42 .diagnostics
43 .iter()
44 .fold(
45 CliReport::build(CliReportKind::Warning, src_id, offset)
46 .with_config(
47 CliConfig::default()
48 .with_cross_gap(true)
49 .with_multiline_arrows(false)
50 .with_label_attach(LabelAttach::Middle)
51 .with_char_set(CharSet::Unicode),
52 )
53 .with_message(report.note)
54 .with_code(report.code),
55 |cli_report, diagnostic| {
56 cli_report.with_label(
57 Label::new((src_id, range(diagnostic.at)))
58 .with_message(&colorize(&diagnostic.message))
59 .with_color(Color::Magenta),
60 )
61 },
62 )
63 .finish()
64 .write((src_id, Source::from(src)), &mut *self)?;
65 }
66 Ok(())
67 }
68}
69
70// everything within backticks is colorized, backticks are removed
71fn colorize(message: &str) -> String {
72 message.split('`')
73 .enumerate()
74 .map(|(idx, part)| {
75 if idx % 2 == 1 {
76 part.fg(Color::Cyan).to_string()
77 } else {
78 part.to_string()
79 }
80 })
81 .collect::<Vec<_>>()
82 .join("")
83}