|
| 1 | +//! Implementation of the check and lint commands. |
| 2 | +
|
| 3 | +use std::borrow::Cow; |
1 | 4 | use std::path::PathBuf;
|
2 | 5 |
|
3 | 6 | use anyhow::bail;
|
| 7 | +use anyhow::Context; |
4 | 8 | use clap::Parser;
|
5 | 9 | use clap::ValueEnum;
|
| 10 | +use codespan_reporting::files::SimpleFile; |
| 11 | +use codespan_reporting::term::emit; |
6 | 12 | use codespan_reporting::term::termcolor::ColorChoice;
|
7 | 13 | use codespan_reporting::term::termcolor::StandardStream;
|
8 | 14 | use codespan_reporting::term::Config;
|
9 | 15 | 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. |
11 | 26 | #[derive(Clone, Debug, Default, ValueEnum)]
|
12 | 27 | pub enum Mode {
|
13 | 28 | /// Prints diagnostics as multiple lines.
|
@@ -52,50 +67,165 @@ pub struct Common {
|
52 | 67 | #[derive(Parser, Debug)]
|
53 | 68 | #[command(author, version, about)]
|
54 | 69 | pub struct CheckArgs {
|
| 70 | + /// The common command line arguments. |
55 | 71 | #[command(flatten)]
|
56 | 72 | common: Common,
|
57 | 73 |
|
58 |
| - /// Perform lint checks in addition to syntax validation. |
| 74 | + /// Perform lint checks in addition to checking for errors. |
59 | 75 | #[arg(short, long)]
|
60 | 76 | 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, |
61 | 85 | }
|
62 | 86 |
|
63 | 87 | /// Arguments for the `lint` subcommand.
|
64 | 88 | #[derive(Parser, Debug)]
|
65 | 89 | #[command(author, version, about)]
|
66 | 90 | pub struct LintArgs {
|
| 91 | + /// The command command line arguments. |
67 | 92 | #[command(flatten)]
|
68 | 93 | 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, |
69 | 102 | }
|
70 | 103 |
|
71 |
| -pub fn check(args: CheckArgs) -> anyhow::Result<()> { |
| 104 | +/// Checks WDL source files for diagnostics. |
| 105 | +pub async fn check(args: CheckArgs) -> anyhow::Result<()> { |
72 | 106 | 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 | + } |
74 | 195 | }
|
75 | 196 |
|
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 | + ); |
87 | 212 | }
|
88 | 213 |
|
89 | 214 | Ok(())
|
90 | 215 | }
|
91 | 216 |
|
92 |
| -pub fn lint(args: LintArgs) -> anyhow::Result<()> { |
| 217 | +/// Lints WDL source files. |
| 218 | +pub async fn lint(args: LintArgs) -> anyhow::Result<()> { |
93 | 219 | check(CheckArgs {
|
94 | 220 | common: args.common,
|
95 | 221 | lint: true,
|
| 222 | + deny_warnings: args.deny_warnings, |
| 223 | + deny_notes: args.deny_notes, |
96 | 224 | })
|
| 225 | + .await |
97 | 226 | }
|
98 | 227 |
|
| 228 | +/// Gets the display config to use for reporting diagnostics. |
99 | 229 | fn get_display_config(args: &Common) -> (Config, StandardStream) {
|
100 | 230 | let display_style = match args.report_mode {
|
101 | 231 | Mode::Full => DisplayStyle::Rich,
|
|
0 commit comments