Skip to content

Commit b266a4a

Browse files
authored
feat: improvements to the check command. (#18)
* feat: improvements to the `check` command. This commit contains the following improvements: * use `wdl-analysis` for the `check` command that will perform a full static-analysis on the specified file or directory. * add a progress bar to display for the `check` command. * use `clap-verbosity-flag` for the `quiet` and `verbose` flags; this has the advantage of being error level by default and each `-v` specified drops the log level down by one; this gives a little more control over the previous implementation that supported either info (default), debug (-v), or error (-q). * we now colorize the `error` part of handling errors from command execution, displaying the full context of the error, and exiting with status code 1. * feat: add `--deny-notes` option to the `check` command. * feat: make the `lint` command also perform an analysis. * chore: code cleanup and removal of unused code. This commit also exposes the commands as part of the `sprocket` crate from which `main.rs` will use.
1 parent 77e7183 commit b266a4a

File tree

11 files changed

+249
-315
lines changed

11 files changed

+249
-315
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Changed
11+
12+
* Implemented the `check` command as a full static analysis ([#17](https://github.com/stjude-rust-labs/sprocket/pull/17)).
13+
1014
## 0.6.0 - 08-22-2024
1115

1216
### Added

Cargo.lock

Lines changed: 65 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ nonempty = "0.10.0"
2020
pest = { version = "2.7.11", features = ["pretty-print"] }
2121
tracing = "0.1.40"
2222
tracing-subscriber = "0.3.18"
23-
wdl = { version = "0.7.0", features = ["codespan", "lsp"] }
23+
wdl = { version = "0.7.0", features = ["codespan", "lsp", "analysis"] }
2424
walkdir = "2.5.0"
2525
colored = "2.1.0"
2626
tokio = { version = "1.39.1", features = ["full"] }
2727
tracing-log = "0.2.0"
28+
indicatif = "0.17.8"
29+
clap-verbosity-flag = "2.2.1"

src/commands.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Implementation of sprocket CLI commands.
2+
13
pub mod analyzer;
24
pub mod check;
35
pub mod explain;

src/commands/analyzer.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Implementation of the analyzer command.
2+
13
use clap::Parser;
24
use wdl::lsp::Server;
35
use wdl::lsp::ServerOptions;

src/commands/check.rs

Lines changed: 146 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
1+
//! Implementation of the check and lint commands.
2+
3+
use std::borrow::Cow;
14
use std::path::PathBuf;
25

36
use anyhow::bail;
7+
use anyhow::Context;
48
use clap::Parser;
59
use clap::ValueEnum;
10+
use codespan_reporting::files::SimpleFile;
11+
use codespan_reporting::term::emit;
612
use codespan_reporting::term::termcolor::ColorChoice;
713
use codespan_reporting::term::termcolor::StandardStream;
814
use codespan_reporting::term::Config;
915
use codespan_reporting::term::DisplayStyle;
10-
16+
use indicatif::ProgressBar;
17+
use indicatif::ProgressStyle;
18+
use wdl::analysis::Analyzer;
19+
use wdl::ast::Diagnostic;
20+
use wdl::ast::Severity;
21+
use wdl::ast::SyntaxNode;
22+
use wdl::ast::Validator;
23+
use wdl::lint::LintVisitor;
24+
25+
/// The diagnostic mode to use for reporting diagnostics.
1126
#[derive(Clone, Debug, Default, ValueEnum)]
1227
pub enum Mode {
1328
/// Prints diagnostics as multiple lines.
@@ -52,50 +67,165 @@ pub struct Common {
5267
#[derive(Parser, Debug)]
5368
#[command(author, version, about)]
5469
pub struct CheckArgs {
70+
/// The common command line arguments.
5571
#[command(flatten)]
5672
common: Common,
5773

58-
/// Perform lint checks in addition to syntax validation.
74+
/// Perform lint checks in addition to checking for errors.
5975
#[arg(short, long)]
6076
lint: bool,
77+
78+
/// Causes the command to fail if warnings were reported.
79+
#[clap(long)]
80+
deny_warnings: bool,
81+
82+
/// Causes the command to fail if notes were reported.
83+
#[clap(long)]
84+
deny_notes: bool,
6185
}
6286

6387
/// Arguments for the `lint` subcommand.
6488
#[derive(Parser, Debug)]
6589
#[command(author, version, about)]
6690
pub struct LintArgs {
91+
/// The command command line arguments.
6792
#[command(flatten)]
6893
common: Common,
94+
95+
/// Causes the command to fail if warnings were reported.
96+
#[clap(long)]
97+
deny_warnings: bool,
98+
99+
/// Causes the command to fail if notes were reported.
100+
#[clap(long)]
101+
deny_notes: bool,
69102
}
70103

71-
pub fn check(args: CheckArgs) -> anyhow::Result<()> {
104+
/// Checks WDL source files for diagnostics.
105+
pub async fn check(args: CheckArgs) -> anyhow::Result<()> {
72106
if !args.lint && !args.common.except.is_empty() {
73-
bail!("cannot specify --except without --lint");
107+
bail!("cannot specify `--except` without `--lint`");
108+
}
109+
110+
let (config, mut stream) = get_display_config(&args.common);
111+
112+
let lint = args.lint;
113+
let except_rules = args.common.except;
114+
let analyzer = Analyzer::new_with_validator(
115+
move |bar: ProgressBar, kind, completed, total| async move {
116+
if completed == 0 {
117+
bar.set_length(total.try_into().unwrap());
118+
bar.set_message(format!("{kind}"));
119+
}
120+
bar.set_position(completed.try_into().unwrap());
121+
},
122+
move || {
123+
let mut validator = Validator::empty();
124+
125+
if lint {
126+
let visitor = LintVisitor::new(wdl::lint::rules().into_iter().filter_map(|rule| {
127+
if except_rules.contains(&rule.id().to_string()) {
128+
None
129+
} else {
130+
Some(rule)
131+
}
132+
}));
133+
validator.add_visitor(visitor);
134+
}
135+
136+
validator
137+
},
138+
);
139+
140+
let bar = ProgressBar::new(0);
141+
bar.set_style(
142+
ProgressStyle::with_template("[{elapsed_precise}] {bar:40.cyan/blue} {msg} {pos}/{len}")
143+
.unwrap(),
144+
);
145+
146+
analyzer.add_documents(args.common.paths).await?;
147+
let results = analyzer
148+
.analyze(bar.clone())
149+
.await
150+
.context("failed to analyze documents")?;
151+
152+
// Drop (hide) the progress bar before emitting any diagnostics
153+
drop(bar);
154+
155+
let cwd = std::env::current_dir().ok();
156+
let mut error_count = 0;
157+
let mut warning_count = 0;
158+
let mut note_count = 0;
159+
for result in &results {
160+
let path = result.uri().to_file_path().ok();
161+
162+
// Attempt to strip the CWD from the result path
163+
let path = match (&cwd, &path) {
164+
// Only display diagnostics for local files.
165+
(_, None) => continue,
166+
// Use just the path if there's no CWD
167+
(None, Some(path)) => path.to_string_lossy(),
168+
// Strip the CWD from the path
169+
(Some(cwd), Some(path)) => path.strip_prefix(cwd).unwrap_or(path).to_string_lossy(),
170+
};
171+
172+
let diagnostics: Cow<'_, [Diagnostic]> = match result.parse_result().error() {
173+
Some(e) => vec![Diagnostic::error(format!("failed to read `{path}`: {e:#}"))].into(),
174+
None => result.diagnostics().into(),
175+
};
176+
177+
if !diagnostics.is_empty() {
178+
let source = result
179+
.parse_result()
180+
.root()
181+
.map(|n| SyntaxNode::new_root(n.clone()).text().to_string())
182+
.unwrap_or(String::new());
183+
let file = SimpleFile::new(path, source);
184+
for diagnostic in diagnostics.iter() {
185+
match diagnostic.severity() {
186+
Severity::Error => error_count += 1,
187+
Severity::Warning => warning_count += 1,
188+
Severity::Note => note_count += 1,
189+
}
190+
191+
emit(&mut stream, &config, &file, &diagnostic.to_codespan())
192+
.context("failed to emit diagnostic")?;
193+
}
194+
}
74195
}
75196

76-
let (config, writer) = get_display_config(&args.common);
77-
78-
match sprocket::file::Repository::try_new(args.common.paths, vec!["wdl".to_string()])?
79-
.report_diagnostics(config, writer, args.lint, args.common.except)?
80-
{
81-
// There are syntax errors.
82-
(true, _) => std::process::exit(1),
83-
// There are lint failures.
84-
(false, true) => std::process::exit(2),
85-
// There are no diagnostics.
86-
(false, false) => {}
197+
if error_count > 0 {
198+
bail!(
199+
"aborting due to previous {error_count} error{s}",
200+
s = if error_count == 1 { "" } else { "s" }
201+
);
202+
} else if args.deny_warnings && warning_count > 0 {
203+
bail!(
204+
"aborting due to previous {warning_count} warning{s} (`--deny-warnings` was specified)",
205+
s = if warning_count == 1 { "" } else { "s" }
206+
);
207+
} else if args.deny_notes && note_count > 0 {
208+
bail!(
209+
"aborting due to previous {note_count} note{s} (`--deny-notes` was specified)",
210+
s = if note_count == 1 { "" } else { "s" }
211+
);
87212
}
88213

89214
Ok(())
90215
}
91216

92-
pub fn lint(args: LintArgs) -> anyhow::Result<()> {
217+
/// Lints WDL source files.
218+
pub async fn lint(args: LintArgs) -> anyhow::Result<()> {
93219
check(CheckArgs {
94220
common: args.common,
95221
lint: true,
222+
deny_warnings: args.deny_warnings,
223+
deny_notes: args.deny_notes,
96224
})
225+
.await
97226
}
98227

228+
/// Gets the display config to use for reporting diagnostics.
99229
fn get_display_config(args: &Common) -> (Config, StandardStream) {
100230
let display_style = match args.report_mode {
101231
Mode::Full => DisplayStyle::Rich,

src/commands/explain.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Implementation of the explain command.
2+
13
use clap::Parser;
24
use colored::Colorize;
35
use wdl::lint;
@@ -11,6 +13,7 @@ pub struct Args {
1113
pub rule_name: String,
1214
}
1315

16+
/// Lists all rules as a string for displaying after CLI help.
1417
pub fn list_all_rules() -> String {
1518
let mut result = "Available rules:".to_owned();
1619
for rule in lint::rules() {
@@ -19,6 +22,7 @@ pub fn list_all_rules() -> String {
1922
result
2023
}
2124

25+
/// Pretty prints a rule to a string.
2226
pub fn pretty_print_rule(rule: &dyn lint::Rule) -> String {
2327
let mut result = format!("{}", rule.id().bold().underline());
2428
result = format!("{}\n{}", result, rule.description());
@@ -30,6 +34,7 @@ pub fn pretty_print_rule(rule: &dyn lint::Rule) -> String {
3034
}
3135
}
3236

37+
/// Explains a lint rule.
3338
pub fn explain(args: Args) -> anyhow::Result<()> {
3439
let name = args.rule_name;
3540
let lowercase_name = name.to_lowercase();

0 commit comments

Comments
 (0)