diff --git a/.github/actions/install-shellcheck/action.yaml b/.github/actions/install-shellcheck/action.yaml new file mode 100644 index 000000000..67427252d --- /dev/null +++ b/.github/actions/install-shellcheck/action.yaml @@ -0,0 +1,31 @@ +name: Install ShellCheck +description: "Installs ShellCheck. Supports Ubuntu, macOS, and Windows." +runs: + using: "composite" + steps: + - name: install shellcheck -- Ubuntu + if: runner.os == 'Linux' + shell: bash + run: | + gh release download -R koalaman/shellcheck -p 'shellcheck-v*.linux.x86_64.tar.xz' -O shellcheck-latest.tar.xz + tar -xvf shellcheck-latest.tar.xz --strip-components 1 + chmod +x ./shellcheck + echo ${{github.workspace}} >> "$GITHUB_PATH" + export PATH="$PATH":"$(pwd)" + - name: install shellcheck -- macOS + shell: bash + if: runner.os == 'macOS' + run: | + gh release download -R koalaman/shellcheck -p 'shellcheck-v*.darwin.aarch64.tar.xz' -O shellcheck-latest.tar.xz + tar -xvf shellcheck-latest.tar.xz --strip-components 1 + chmod +x ./shellcheck + echo ${{github.workspace}} >> "$GITHUB_PATH" + - name: install shellcheck -- Windows + if: runner.os == 'Windows' + shell: powershell + run: | + gh release download -R koalaman/shellcheck -p 'shellcheck-v*.zip' -O shellcheck-latest.zip + 7z x shellcheck-latest.zip + New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.local\bin" + Move-Item -Force "shellcheck.exe" "$env:USERPROFILE\.local\bin" + Add-Content $env:GITHUB_PATH "$env:USERPROFILE\.local\bin" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1f01e4b23..59986a4c9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -6,6 +6,9 @@ on: - main pull_request: +env: + GH_TOKEN: ${{ github.token }} + jobs: format: runs-on: ubuntu-latest @@ -39,6 +42,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v4 + - uses: ./.github/actions/install-shellcheck - name: Update Rust run: rustup update stable && rustup default stable - run: cargo test --all --all-features diff --git a/Cargo.toml b/Cargo.toml index 4998b185d..c06ac77c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ parking_lot = "0.12.3" path-clean = "1.0.1" petgraph = "0.6.5" pretty_assertions = "1.4.0" +rand = "0.8.5" rayon = "1.10.0" regex = "1.11.1" reqwest = { version = "0.12.5", default-features = false, features = ["rustls-tls", "http2", "charset"] } diff --git a/gauntlet/CHANGELOG.md b/gauntlet/CHANGELOG.md index e2a17754c..9192b641b 100644 --- a/gauntlet/CHANGELOG.md +++ b/gauntlet/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* `--shellcheck` flag to run shellcheck lints in arena mode ([#264](https://github.com/stjude-rust-labs/wdl/pull/264)) * Full analysis instead of basic validation ([#207](https://github.com/stjude-rust-labs/wdl/pull/172)) * Checkout submodules ([#207](https://github.com/stjude-rust-labs/wdl/pull/172)) diff --git a/gauntlet/src/lib.rs b/gauntlet/src/lib.rs index 4c19b0638..09010b48c 100644 --- a/gauntlet/src/lib.rs +++ b/gauntlet/src/lib.rs @@ -44,6 +44,7 @@ use wdl::analysis::rules; use wdl::ast::Diagnostic; use wdl::lint::LintVisitor; use wdl::lint::ast::Validator; +use wdl::lint::rules::ShellCheckRule; use crate::repository::WorkDir; @@ -124,6 +125,10 @@ pub struct Args { /// Additional information is logged in the console. #[arg(short, long)] pub verbose: bool, + + /// Enable shellcheck lints. + #[arg(long, action, requires = "arena")] + pub shellcheck: bool, } /// Main function for this subcommand. @@ -184,6 +189,9 @@ pub async fn gauntlet(args: Args) -> Result<()> { }; if args.arena { validator.add_visitor(LintVisitor::default()); + if args.shellcheck { + validator.add_visitor(ShellCheckRule); + } } validator diff --git a/wdl-lint/CHANGELOG.md b/wdl-lint/CHANGELOG.md index 84af96752..ad59bf692 100644 --- a/wdl-lint/CHANGELOG.md +++ b/wdl-lint/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Added +* Added a `ShellCheck` rule ([#264](https://github.com/stjude-rust-labs/wdl/pull/264)). * Added a `RedundantInputAssignment` rule ([#244](https://github.com/stjude-rust-labs/wdl/pull/244)). ## Changed diff --git a/wdl-lint/Cargo.toml b/wdl-lint/Cargo.toml index 70bb16ec8..cc1d12119 100644 --- a/wdl-lint/Cargo.toml +++ b/wdl-lint/Cargo.toml @@ -13,9 +13,14 @@ readme = "../README.md" [dependencies] wdl-ast = { path = "../wdl-ast", version = "0.9.0" } +anyhow = { workspace = true } convert_case = { workspace = true } indexmap = { workspace = true } +rand = { workspace = true } rowan = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tracing = { workspace = true } [dev-dependencies] codespan-reporting = { workspace = true } diff --git a/wdl-lint/RULES.md b/wdl-lint/RULES.md index adaa862cd..4ac5098ae 100644 --- a/wdl-lint/RULES.md +++ b/wdl-lint/RULES.md @@ -43,6 +43,7 @@ be out of sync with released packages. | `RuntimeSectionKeys` | Completeness, Deprecated | Ensures that runtime sections have the appropriate keys. | | `RedundantInputAssignment` | Style | Ensures that redundant input assignments are shortened | | `SectionOrdering` | Sorting, Style | Ensures that sections within tasks and workflows are sorted. | +| `ShellCheck` | Correctness, Portability | (BETA) Ensures that command sections are free of shellcheck diagnostics. | | `SnakeCase` | Clarity, Naming, Style | Ensures that tasks, workflows, and variables are defined with snake_case names. | | `Todo` | Completeness | Ensures that `TODO` statements are flagged for followup. | | `TrailingComma` | Style | Ensures that lists and objects in meta have a trailing comma. | diff --git a/wdl-lint/src/lib.rs b/wdl-lint/src/lib.rs index 8362dae1f..cffdc5bc7 100644 --- a/wdl-lint/src/lib.rs +++ b/wdl-lint/src/lib.rs @@ -150,3 +150,34 @@ pub fn rules() -> Vec> { rules } + +/// Gets the optional rule set. +pub fn optional_rules() -> Vec> { + let opt_rules: Vec> = vec![Box::::default()]; + + // Ensure all the rule ids are unique and pascal case + #[cfg(debug_assertions)] + { + use convert_case::Case; + use convert_case::Casing; + + use crate::rules; + let mut set: std::collections::HashSet<&str> = + std::collections::HashSet::from_iter(rules().iter().map(|r| r.id())); + for r in opt_rules.iter() { + if r.id().to_case(Case::Pascal) != r.id() { + panic!("lint rule id `{id}` is not pascal case", id = r.id()); + } + + if !set.insert(r.id()) { + panic!("duplicate rule id `{id}`", id = r.id()); + } + + if RESERVED_RULE_IDS.contains(&r.id()) { + panic!("rule id `{id}` is reserved", id = r.id()); + } + } + } + + opt_rules +} diff --git a/wdl-lint/src/rules.rs b/wdl-lint/src/rules.rs index 9d49dfdde..564f4aef4 100644 --- a/wdl-lint/src/rules.rs +++ b/wdl-lint/src/rules.rs @@ -35,6 +35,7 @@ mod preamble_formatting; mod redundant_input_assignment; mod runtime_section_keys; mod section_order; +mod shellcheck; mod snake_case; mod todo; mod trailing_comma; @@ -77,6 +78,7 @@ pub use preamble_formatting::*; pub use redundant_input_assignment::*; pub use runtime_section_keys::*; pub use section_order::*; +pub use shellcheck::*; pub use snake_case::*; pub use todo::*; pub use trailing_comma::*; diff --git a/wdl-lint/src/rules/misplaced_lint_directive.rs b/wdl-lint/src/rules/misplaced_lint_directive.rs index 750ce1ffb..2e8591121 100644 --- a/wdl-lint/src/rules/misplaced_lint_directive.rs +++ b/wdl-lint/src/rules/misplaced_lint_directive.rs @@ -20,6 +20,7 @@ use wdl_ast::Visitor; use crate::Rule; use crate::Tag; use crate::TagSet; +use crate::optional_rules; use crate::rules; /// The identifier for the unknown rule rule. @@ -60,6 +61,10 @@ pub static RULE_MAP: LazyLock = OnceLock::new(); + +/// The identifier for the command section ShellCheck rule. +const ID: &str = "ShellCheck"; + +/// A ShellCheck diagnostic. +/// +/// The `file` and `fix` fields are ommitted as we have no use for them. +#[derive(Clone, Debug, Deserialize)] +struct ShellCheckDiagnostic { + /// line number comment starts on + pub line: usize, + /// line number comment ends on + #[serde(rename = "endLine")] + pub end_line: usize, + /// column comment starts on + pub column: usize, + /// column comment ends on + #[serde(rename = "endColumn")] + pub end_column: usize, + /// severity of the comment + pub level: String, + /// shellcheck error code + pub code: usize, + /// message associated with the comment + pub message: String, +} + +/// Run shellcheck on a command. +/// +/// writes command text to stdin of shellcheck process +/// and returns parsed `ShellCheckDiagnostic`s +fn run_shellcheck(command: &str) -> Result> { + let mut sc_proc = process::Command::new(SHELLCHECK_BIN) + .args([ + "-s", // bash shell + "bash", + "-f", // output JSON + "json", + "-e", // errors to suppress + &SHELLCHECK_SUPPRESS.join(","), + "-S", // set minimum lint level to style + "style", + "-", // input is piped to STDIN + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .context("spawning the `shellcheck` process")?; + debug!("`shellcheck` process id: {}", sc_proc.id()); + { + let mut proc_stdin = sc_proc + .stdin + .take() + .context("obtaining the STDIN handle of the `shellcheck` process")?; + proc_stdin.write_all(command.as_bytes())?; + } + + let output = sc_proc + .wait_with_output() + .context("waiting for the `shellcheck` process to complete")?; + + // shellcheck returns exit code 1 if + // any checked files result in comments + // so cannot check with status.success() + match output.status.code() { + Some(0) | Some(1) => serde_json::from_slice::>(&output.stdout) + .context("deserializing STDOUT from `shellcheck` process"), + Some(code) => bail!("unexpected `shellcheck` exit code: {}", code), + None => bail!("the `shellcheck` process appears to have been interrupted"), + } +} + +/// Runs ShellCheck on a command section and reports diagnostics. +#[derive(Default, Debug, Clone, Copy)] +pub struct ShellCheckRule; + +impl Rule for ShellCheckRule { + fn id(&self) -> &'static str { + ID + } + + fn description(&self) -> &'static str { + "Ensures that command blocks are free of ShellCheck violations." + } + + fn explanation(&self) -> &'static str { + "ShellCheck (https://shellcheck.net) is a static analysis tool and linter for sh / bash. \ + The lints provided by ShellCheck help prevent common errors and pitfalls in your scripts. \ + Following its recommendations will increase the robustness of your command sections." + } + + fn tags(&self) -> TagSet { + TagSet::new(&[Tag::Correctness, Tag::Portability]) + } + + fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> { + Some(&[ + SyntaxKind::VersionStatementNode, + SyntaxKind::CommandSectionNode, + ]) + } +} + +/// Convert a WDL `Placeholder` to a bash variable declaration. +/// +/// Returns "WDL" + random alphnumeric characters. +/// The returned value is shorter than the placeholder by 3 characters so +/// that the caller may pad with other characters as necessary +/// depending on whether or not the variable needs to be treated as a +/// declaration, expansion, or literal. +fn to_bash_var(placeholder: &Placeholder) -> String { + let placeholder_len: usize = placeholder.syntax().text_range().len().into(); + // don't start variable with numbers + let mut bash_var = String::from("WDL"); + bash_var.push_str( + &Alphanumeric.sample_string(&mut rand::thread_rng(), placeholder_len.saturating_sub(6)), + ); + bash_var +} + +/// Retrieve all input and private declarations for a task. +fn gather_task_declarations(task: &TaskDefinition) -> HashSet { + let mut decls = HashSet::new(); + if let Some(input) = task.input() { + for decl in input.declarations() { + decls.insert(decl.name().as_str().to_owned()); + } + } + + for decl in task.declarations() { + decls.insert(decl.name().as_str().to_owned()); + } + decls +} + +/// Creates a "ShellCheck lint" diagnostic from a `ShellCheckDiagnostic` +fn shellcheck_lint(diagnostic: &ShellCheckDiagnostic, span: Span) -> Diagnostic { + let label = format!( + "SC{}[{}]: {}", + diagnostic.code, diagnostic.level, diagnostic.message + ); + Diagnostic::note(&diagnostic.message) + .with_rule(ID) + .with_label(label, span) + .with_label( + format!("more info: {}/SC{}", &SHELLCHECK_WIKI, diagnostic.code), + span, + ) + .with_fix("address the diagnostic as recommended in the message") +} + +/// Sanitize a `CommandSection`. +/// +/// Removes all trailing whitespace, replaces placeholders +/// with dummy bash variables or literals, and records declarations. +/// +/// If the section contains mixed indentation, returns None. +fn sanitize_command(section: &CommandSection) -> Option<(String, HashSet)> { + let mut sanitized_command = String::new(); + let mut decls = HashSet::new(); + let mut needs_quotes = true; + let mut is_literal = false; + if let Some(cmd_parts) = section.strip_whitespace() { + cmd_parts.iter().for_each(|part| match part { + StrippedCommandPart::Text(text) => { + sanitized_command.push_str(text); + // if this placeholder is in a single-quoted segment + // don't treat as an expansion but rather a literal. + is_literal ^= !is_properly_quoted(text, '\''); + // if this text section is not properly quoted then the + // next placeholder does *not* need double quotes + // because it will end up enclosed. + needs_quotes ^= !is_properly_quoted(text, '"'); + } + StrippedCommandPart::Placeholder(placeholder) => { + let bash_var = to_bash_var(placeholder); + // we need to save the var so we can suppress later + decls.insert(bash_var.clone()); + + if is_literal { + // pad literal with three underscores to account for ~{} + sanitized_command.push_str(&format!("___{bash_var}")); + } else if needs_quotes { + // surround with quotes for proper form + sanitized_command.push_str(&format!("\"${bash_var}\"")); + } else { + // surround with curly braces because already + // inside of a quoted segment. + sanitized_command.push_str(&format!("${{{bash_var}}}")); + } + } + }); + Some((sanitized_command, decls)) + } else { + None + } +} + +/// Maps each line as shellcheck sees it to its corresponding start position in +/// the source. +fn map_shellcheck_lines(section: &CommandSection) -> HashMap { + let mut line_map = HashMap::new(); + let mut line_num = 1; + let mut skip_next_line = false; + for part in section.parts() { + match part { + CommandPart::Text(ref text) => { + for (line, line_start, _) in lines_with_offset(text.as_str()) { + // this occurs after encountering a placeholder + if skip_next_line { + skip_next_line = false; + continue; + } + // Add back leading whitespace that is stripped from the sanitized command. + // The first line is removed entirely, UNLESS there is content on it. + let leading_ws = if line_num > 1 || !line.trim().is_empty() { + count_leading_whitespace(line) + } else { + continue; + }; + let adjusted_start = text.span().start() + line_start + leading_ws; + line_map.insert(line_num, adjusted_start); + line_num += 1; + } + } + CommandPart::Placeholder(_) => { + skip_next_line = true; + } + } + } + line_map +} + +/// Calculates the correct `Span` for a `ShellCheckDiagnostic` relative to the +/// source. +fn calculate_span(diagnostic: &ShellCheckDiagnostic, line_map: &HashMap) -> Span { + // shellcheck 1-indexes columns, so subtract 1. + let start = line_map + .get(&diagnostic.line) + .expect("shellcheck line corresponds to command line") + + diagnostic.column + - 1; + let len = if diagnostic.end_line > diagnostic.line { + // this is a multiline diagnostic + let end_line_end = line_map + .get(&diagnostic.end_line) + .expect("shellcheck line corresponds to command line") + + diagnostic.end_column + - 1; + // - 2 to discount first and last newlines + end_line_end.saturating_sub(start) - 2 + } else { + // single line diagnostic + (diagnostic.end_column).saturating_sub(diagnostic.column) + }; + Span::new(start, len) +} + +impl Visitor for ShellCheckRule { + type State = Diagnostics; + + fn document( + &mut self, + _: &mut Self::State, + reason: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + if reason == VisitReason::Exit { + return; + } + + // Reset the visitor upon document entry + *self = Default::default(); + } + + fn command_section( + &mut self, + state: &mut Self::State, + reason: VisitReason, + section: &CommandSection, + ) { + if reason == VisitReason::Exit { + return; + } + + if !SHELLCHECK_EXISTS.get_or_init(|| { + if !program_exists(SHELLCHECK_BIN) { + let command_keyword = support::token(section.syntax(), SyntaxKind::CommandKeyword) + .expect( + "should have a + command keyword token", + ); + state.exceptable_add( + Diagnostic::note("running `shellcheck` on command section") + .with_label( + "could not find `shellcheck` executable.", + command_keyword.text_range().to_span(), + ) + .with_rule(ID) + .with_fix( + "install shellcheck (https://www.shellcheck.net) or disable this lint.", + ), + SyntaxElement::from(section.syntax().clone()), + &self.exceptable_nodes(), + ); + return false; + } + true + }) { + return; + } + + // Collect declarations so we can ignore placeholder variables + let parent_task = section.parent().into_task().expect("parent is a task"); + let mut decls = gather_task_declarations(&parent_task); + + // Replace all placeholders in the command with dummy bash variables + let Some((sanitized_command, cmd_decls)) = sanitize_command(section) else { + // This is the case where the command section contains + // mixed indentation. We silently return and allow + // the mixed indentation lint to report this. + return; + }; + decls.extend(cmd_decls); + let line_map = map_shellcheck_lines(section); + + match run_shellcheck(&sanitized_command) { + Ok(diagnostics) => { + for diagnostic in diagnostics { + // Skip declarations that shellcheck is unaware of. + // ShellCheck's message always starts with the variable name + // that is unassigned. + let target_variable = + diagnostic.message.split_whitespace().next().unwrap_or(""); + if diagnostic.code == SHELLCHECK_REFERENCED_UNASSIGNED + && decls.contains(target_variable) + { + continue; + } + let span = calculate_span(&diagnostic, &line_map); + state.exceptable_add( + shellcheck_lint(&diagnostic, span), + SyntaxElement::from(section.syntax().clone()), + &self.exceptable_nodes(), + ) + } + } + Err(e) => { + let command_keyword = support::token(section.syntax(), SyntaxKind::CommandKeyword) + .expect("should have a command keyword token"); + state.exceptable_add( + Diagnostic::error("running `shellcheck` on command section") + .with_label(e.to_string(), command_keyword.text_range().to_span()) + .with_rule(ID) + .with_fix("address reported error."), + SyntaxElement::from(section.syntax().clone()), + &self.exceptable_nodes(), + ); + } + } + } +} diff --git a/wdl-lint/src/util.rs b/wdl-lint/src/util.rs index 8220afc05..d676838a4 100644 --- a/wdl-lint/src/util.rs +++ b/wdl-lint/src/util.rs @@ -1,9 +1,19 @@ //! A module for utility functions for the lint rules. +use std::process::Command; +use std::process::Stdio; + use wdl_ast::AstToken; use wdl_ast::Comment; use wdl_ast::SyntaxKind; +/// Counts the amount of leading whitespace in a string slice. +/// +/// Returns when the first non-whitespace character is encountered. +pub fn count_leading_whitespace(input: &str) -> usize { + input.chars().take_while(|ch| ch.is_whitespace()).count() +} + /// Detect if a comment is in-line or not by looking for `\n` in the prior /// whitespace. pub fn is_inline_comment(token: &Comment) -> bool { @@ -24,6 +34,23 @@ pub fn is_inline_comment(token: &Comment) -> bool { false } +/// Determines whether or not a string containing embedded quotes is properly +/// quoted. +pub fn is_properly_quoted(s: &str, quote_char: char) -> bool { + let mut closed = true; + let mut escaped = false; + s.chars().for_each(|c| { + if c == '\\' { + escaped = true; + } else if !escaped && c == quote_char { + closed = !closed; + } else { + escaped = false; + } + }); + closed +} + /// Iterates over the lines of a string and returns the line, starting offset, /// and next possible starting offset. pub fn lines_with_offset(s: &str) -> impl Iterator { @@ -60,6 +87,21 @@ pub fn lines_with_offset(s: &str) -> impl Iterator }) } +/// Check whether or not a program exists. +/// +/// On unix-like OSes, uses `which`. +/// On Windows, uses `where.exe`. +pub fn program_exists(exec: &str) -> bool { + let finder = if cfg!(windows) { "where.exe" } else { "which" }; + Command::new(finder) + .arg(exec) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|r| r.success()) +} + /// Strips a single newline from the end of a string. pub fn strip_newline(s: &str) -> Option<&str> { s.strip_suffix("\r\n").or_else(|| s.strip_suffix('\n')) @@ -145,4 +187,45 @@ task foo { # an in-line comment ("and even a \r that should not be a newline", 53, 95), ]); } + + #[test] + fn test_count_leading_whitespace() { + let s = " this string has four leading spaces"; + assert_eq!(count_leading_whitespace(s), 4); + let s = "\t\t\t\tthis string has four leading tabs"; + assert_eq!(count_leading_whitespace(s), 4); + let s = "\r\r\r\rthis has four leading carriage returns"; + assert_eq!(count_leading_whitespace(s), 4); + let s = "\n starts with a newline"; + assert_eq!(count_leading_whitespace(s), 2); + let s = "I have no leading whitespace"; + assert_eq!(count_leading_whitespace(s), 0); + } + + #[test] + fn test_program_exists() { + if cfg!(windows) { + assert!(program_exists("where.exe")); + } else { + assert!(program_exists("which")); + } + } + + #[test] + fn test_is_properly_quoted() { + let s = "\"this string is quoted properly.\""; + assert!(is_properly_quoted(s, '"')); + let s = "\"this string has an escaped \\\" quote.\""; + assert!(is_properly_quoted(s, '"')); + let s = "\"this string is missing an end quote"; + assert_eq!(is_properly_quoted(s, '"'), false); + let s = "this string is missing an open quote\""; + assert_eq!(is_properly_quoted(s, '"'), false); + let s = "\"this string has an irrelevant escape \\ \""; + assert!(is_properly_quoted(s, '"')); + let s = "'this string has single quotes'"; + assert!(is_properly_quoted(s, '\'')); + let s = "this string has unclosed single quotes'"; + assert_eq!(is_properly_quoted(s, '\''), false); + } } diff --git a/wdl-lint/tests/lints.rs b/wdl-lint/tests/lints.rs index 12b4d761e..9cd0aa85e 100644 --- a/wdl-lint/tests/lints.rs +++ b/wdl-lint/tests/lints.rs @@ -32,6 +32,7 @@ use wdl_ast::Diagnostic; use wdl_ast::Document; use wdl_ast::Validator; use wdl_lint::LintVisitor; +use wdl_lint::rules::ShellCheckRule; /// Finds tests for this package. fn find_tests() -> Vec { @@ -141,6 +142,7 @@ fn run_test(test: &Path, ntests: &AtomicUsize) -> Result<(), String> { } else { let mut validator = Validator::default(); validator.add_visitor(LintVisitor::default()); + validator.add_visitor(ShellCheckRule::default()); let errors = match validator.validate(&document) { Ok(()) => String::new(), Err(diagnostics) => format_diagnostics(&diagnostics, &path, &source), diff --git a/wdl-lint/tests/lints/shellcheck-error/source.errors b/wdl-lint/tests/lints/shellcheck-error/source.errors new file mode 100644 index 000000000..c5471c0cd --- /dev/null +++ b/wdl-lint/tests/lints/shellcheck-error/source.errors @@ -0,0 +1,66 @@ +note[ShellCheck]: Couldn't parse this test expression. Fix to allow more checks. + ┌─ tests/lints/shellcheck-error/source.wdl:18:10 + │ +18 │ if [ -f "$broken"] + │ ^ + │ │ + │ SC1073[error]: Couldn't parse this test expression. Fix to allow more checks. + │ more info: https://www.shellcheck.net/wiki/SC1073 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Expected this to be an argument to the unary condition. + ┌─ tests/lints/shellcheck-error/source.wdl:18:15 + │ +18 │ if [ -f "$broken"] + │ ^ + │ │ + │ SC1019[error]: Expected this to be an argument to the unary condition. + │ more info: https://www.shellcheck.net/wiki/SC1019 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: You need a space before the ]. + ┌─ tests/lints/shellcheck-error/source.wdl:18:25 + │ +18 │ if [ -f "$broken"] + │ ^ + │ + │ SC1020[error]: You need a space before the ]. + │ more info: https://www.shellcheck.net/wiki/SC1020 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Couldn't parse this test expression. Fix to allow more checks. + ┌─ tests/lints/shellcheck-error/source.wdl:37:10 + │ +37 │ if [ -f "$broken"] + │ ^ + │ │ + │ SC1073[error]: Couldn't parse this test expression. Fix to allow more checks. + │ more info: https://www.shellcheck.net/wiki/SC1073 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Expected this to be an argument to the unary condition. + ┌─ tests/lints/shellcheck-error/source.wdl:37:15 + │ +37 │ if [ -f "$broken"] + │ ^ + │ │ + │ SC1019[error]: Expected this to be an argument to the unary condition. + │ more info: https://www.shellcheck.net/wiki/SC1019 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: You need a space before the ]. + ┌─ tests/lints/shellcheck-error/source.wdl:37:25 + │ +37 │ if [ -f "$broken"] + │ ^ + │ + │ SC1020[error]: You need a space before the ]. + │ more info: https://www.shellcheck.net/wiki/SC1020 + │ + = fix: address the diagnostic as recommended in the message + diff --git a/wdl-lint/tests/lints/shellcheck-error/source.wdl b/wdl-lint/tests/lints/shellcheck-error/source.wdl new file mode 100644 index 000000000..63525fff4 --- /dev/null +++ b/wdl-lint/tests/lints/shellcheck-error/source.wdl @@ -0,0 +1,43 @@ +#@ except: DescriptionMissing, RuntimeSectionKeys, MatchingParameterMeta, NoCurlyCommands + +## This is a test of having shellcheck error lints + +version 1.1 + +task test1 { + meta {} + + parameter_meta {} + + input { + Int placeholder + } + + command <<< + somecommand.py [[ -f $broken_test]] + if [ -f "$broken"] + >>> + + output {} + + runtime {} +} + +task test2 { + meta {} + + parameter_meta {} + + input { + Int placeholder + } + + command { + somecommand.py [[ -f $broken_test]] + if [ -f "$broken"] + } + + output {} + + runtime {} +} diff --git a/wdl-lint/tests/lints/shellcheck-ok/source.errors b/wdl-lint/tests/lints/shellcheck-ok/source.errors new file mode 100644 index 000000000..e69de29bb diff --git a/wdl-lint/tests/lints/shellcheck-ok/source.wdl b/wdl-lint/tests/lints/shellcheck-ok/source.wdl new file mode 100644 index 000000000..de3769ca3 --- /dev/null +++ b/wdl-lint/tests/lints/shellcheck-ok/source.wdl @@ -0,0 +1,101 @@ +#@ except: DescriptionMissing, RuntimeSectionKeys, MatchingParameterMeta, NoCurlyCommands + +## This is a test of having no shellcheck lints + +version 1.1 + +task test1 { + meta {} + + parameter_meta {} + + input { + Boolean i_quote_my_shellvars + Int placeholder + } + + command <<< + set -eo pipefail + + echo "$placeholder" + + if [[ "$i_quote_my_shellvars" ]]; then + echo "shellcheck will be happy" + fi + >>> + + output {} + + runtime {} +} + +task test2 { + meta {} + + parameter_meta {} + + input { + Int placeholder + } + + command { + set -eo pipefail + + echo "$placeholder" + + if [[ "$I_quote_my_shellvars" ]]; then + echo "all is well" + fi + } + + output {} + + runtime {} +} + +task test3 { + meta {} + + parameter_meta {} + + input { + Int placeholder + } + + #@ except: ShellCheck + command { + set -eo pipefail + + echo "$placeholder" + + if [[ $I_really_want_this_unquoted ]]; then + echo "all is not well" + fi + } + + output {} + + runtime {} +} + +task test4 { + meta {} + + parameter_meta {} + + input { + Int placeholder + } + + command { + set -eo pipefail + + unquoted_var="foo bar baz" + # shellcheck disable=SC2086 + echo $unquoted_var + } + + output {} + + runtime {} +} diff --git a/wdl-lint/tests/lints/shellcheck-style/source.errors b/wdl-lint/tests/lints/shellcheck-style/source.errors new file mode 100644 index 000000000..bc53fec1d --- /dev/null +++ b/wdl-lint/tests/lints/shellcheck-style/source.errors @@ -0,0 +1,44 @@ +note[ShellCheck]: Use 'false' instead of empty [/[[ conditionals. + ┌─ tests/lints/shellcheck-style/source.wdl:17:7 + │ +17 │ [[ ]] + │ ^^^ + │ │ + │ SC2212[style]: Use 'false' instead of empty [/[[ conditionals. + │ more info: https://www.shellcheck.net/wiki/SC2212 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Instead of '[ true ]', just use 'true'. + ┌─ tests/lints/shellcheck-style/source.wdl:18:9 + │ +18 │ [ true ] + │ ^^^^ + │ │ + │ SC2160[style]: Instead of '[ true ]', just use 'true'. + │ more info: https://www.shellcheck.net/wiki/SC2160 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Use 'false' instead of empty [/[[ conditionals. + ┌─ tests/lints/shellcheck-style/source.wdl:36:7 + │ +36 │ [[ ]] + │ ^^^ + │ │ + │ SC2212[style]: Use 'false' instead of empty [/[[ conditionals. + │ more info: https://www.shellcheck.net/wiki/SC2212 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Instead of '[ true ]', just use 'true'. + ┌─ tests/lints/shellcheck-style/source.wdl:37:9 + │ +37 │ [ true ] + │ ^^^^ + │ │ + │ SC2160[style]: Instead of '[ true ]', just use 'true'. + │ more info: https://www.shellcheck.net/wiki/SC2160 + │ + = fix: address the diagnostic as recommended in the message + diff --git a/wdl-lint/tests/lints/shellcheck-style/source.wdl b/wdl-lint/tests/lints/shellcheck-style/source.wdl new file mode 100644 index 000000000..4d3795289 --- /dev/null +++ b/wdl-lint/tests/lints/shellcheck-style/source.wdl @@ -0,0 +1,43 @@ +#@ except: DescriptionMissing, RuntimeSectionKeys, MatchingParameterMeta, NoCurlyCommands + +## This is a test of having shellcheck style lints + +version 1.1 + +task test1 { + meta {} + + parameter_meta {} + + input { + Int placeholder + } + + command <<< + [[ ]] + [ true ] + >>> + + output {} + + runtime {} +} + +task test2 { + meta {} + + parameter_meta {} + + input { + Int placeholder + } + + command { + [[ ]] + [ true ] + } + + output {} + + runtime {} +} diff --git a/wdl-lint/tests/lints/shellcheck-warn/source.errors b/wdl-lint/tests/lints/shellcheck-warn/source.errors new file mode 100644 index 000000000..1781d2ffc --- /dev/null +++ b/wdl-lint/tests/lints/shellcheck-warn/source.errors @@ -0,0 +1,748 @@ +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:17:22 + │ +17 │ somecommand.py $line17 ~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line17 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:17:22 + │ +17 │ somecommand.py $line17 ~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line17 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:18:37 + │ +18 │ somecommand.py ~{placeholder} $line18 + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line18 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:18:37 + │ +18 │ somecommand.py ~{placeholder} $line18 + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line18 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:19:36 + │ +19 │ somecommand.py ~{placeholder}$line19 + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line19 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:19:36 + │ +19 │ somecommand.py ~{placeholder}$line19 + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line19 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:30:22 + │ +30 │ somecommand.py $line30~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line30 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:30:22 + │ +30 │ somecommand.py $line30~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line30 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:31:27 + │ +31 │ somecommand.py [ -f $line31 ] ~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line31 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:31:27 + │ +31 │ somecommand.py [ -f $line31 ] ~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line31 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:49:22 + │ +49 │ somecommand.py $line49 ~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line49 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:49:22 + │ +49 │ somecommand.py $line49 ~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line49 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:50:37 + │ +50 │ somecommand.py ~{placeholder} $line50 + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line50 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:50:37 + │ +50 │ somecommand.py ~{placeholder} $line50 + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line50 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:51:36 + │ +51 │ somecommand.py ~{placeholder}$line51 + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line51 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:51:36 + │ +51 │ somecommand.py ~{placeholder}$line51 + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line51 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:52:22 + │ +52 │ somecommand.py $line52~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line52 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:52:22 + │ +52 │ somecommand.py $line52~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line52 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:53:27 + │ +53 │ somecommand.py [ -f $bad_test ] ~{placeholder} + │ ^^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: bad_test is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:53:27 + │ +53 │ somecommand.py [ -f $bad_test ] ~{placeholder} + │ ^^^^^^^^^ + │ │ + │ SC2154[warning]: bad_test is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:54:27 + │ +54 │ somecommand.py [ -f $trailing_space ] ~{placeholder} + │ ^^^^^^^^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: trailing_space is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:54:27 + │ +54 │ somecommand.py [ -f $trailing_space ] ~{placeholder} + │ ^^^^^^^^^^^^^^^ + │ │ + │ SC2154[warning]: trailing_space is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:72:22 + │ +72 │ somecommand.py $line72 ~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line72 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:72:22 + │ +72 │ somecommand.py $line72 ~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line72 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:73:37 + │ +73 │ somecommand.py ~{placeholder} $line73 + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line73 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:73:37 + │ +73 │ somecommand.py ~{placeholder} $line73 + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line73 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:74:36 + │ +74 │ somecommand.py ~{placeholder}$line74 + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line74 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:74:36 + │ +74 │ somecommand.py ~{placeholder}$line74 + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line74 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:75:22 + │ +75 │ somecommand.py $line75~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line75 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:75:22 + │ +75 │ somecommand.py $line75~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line75 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:76:22 + │ +76 │ ~{placeholder} $line76_trailing_pholder ~{placeholder} + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line76_trailing_pholder is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:76:22 + │ +76 │ ~{placeholder} $line76_trailing_pholder ~{placeholder} + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + │ │ + │ SC2154[warning]: line76_trailing_pholder is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:77:37 + │ +77 │ ~{placeholder} somecommand.py $leading_pholder + │ ^^^^^^^^^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: leading_pholder is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:77:37 + │ +77 │ ~{placeholder} somecommand.py $leading_pholder + │ ^^^^^^^^^^^^^^^^ + │ │ + │ SC2154[warning]: leading_pholder is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:96:22 + │ +96 │ somecommand.py $line96 ~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line96 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:96:22 + │ +96 │ somecommand.py $line96 ~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line96 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:97:37 + │ +97 │ somecommand.py ~{placeholder} $line97 + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line97 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:97:37 + │ +97 │ somecommand.py ~{placeholder} $line97 + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line97 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:98:36 + │ +98 │ somecommand.py ~{placeholder}$line98 + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line98 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:98:36 + │ +98 │ somecommand.py ~{placeholder}$line98 + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line98 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:99:22 + │ +99 │ somecommand.py $line99~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line99 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:99:22 + │ +99 │ somecommand.py $line99~{placeholder} + │ ^^^^^^^ + │ │ + │ SC2154[warning]: line99 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:100:22 + │ +100 │ ~{placeholder} $line100_trailing_pholder ~{placeholder} + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line100_trailing_pholder is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:100:22 + │ +100 │ ~{placeholder} $line100_trailing_pholder ~{placeholder} + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + │ │ + │ SC2154[warning]: line100_trailing_pholder is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:101:37 + │ +101 │ ~{placeholder} somecommand.py $leading_pholder + │ ^^^^^^^^^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: leading_pholder is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:101:37 + │ +101 │ ~{placeholder} somecommand.py $leading_pholder + │ ^^^^^^^^^^^^^^^^ + │ │ + │ SC2154[warning]: leading_pholder is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:118:34 + │ +118 │ command <<< weird stuff $firstlinelint + │ ^^^^^^^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: firstlinelint is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:118:34 + │ +118 │ command <<< weird stuff $firstlinelint + │ ^^^^^^^^^^^^^^ + │ │ + │ SC2154[warning]: firstlinelint is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:120:22 + │ +120 │ somecommand.py $line120 ~{placeholder} + │ ^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line120 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:120:22 + │ +120 │ somecommand.py $line120 ~{placeholder} + │ ^^^^^^^^ + │ │ + │ SC2154[warning]: line120 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:121:37 + │ +121 │ somecommand.py ~{placeholder} $line121 + │ ^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line121 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:121:37 + │ +121 │ somecommand.py ~{placeholder} $line121 + │ ^^^^^^^^ + │ │ + │ SC2154[warning]: line121 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:122:36 + │ +122 │ somecommand.py ~{placeholder}$line122 + │ ^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line122 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:122:36 + │ +122 │ somecommand.py ~{placeholder}$line122 + │ ^^^^^^^^ + │ │ + │ SC2154[warning]: line122 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:123:22 + │ +123 │ somecommand.py $line123~{placeholder} + │ ^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line123 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:123:22 + │ +123 │ somecommand.py $line123~{placeholder} + │ ^^^^^^^^ + │ │ + │ SC2154[warning]: line123 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:124:22 + │ +124 │ ~{placeholder} $line124_trailing_pholder ~{placeholder} + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: line124_trailing_pholder is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:124:22 + │ +124 │ ~{placeholder} $line124_trailing_pholder ~{placeholder} + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + │ │ + │ SC2154[warning]: line124_trailing_pholder is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:126:37 + │ +126 │ ~{placeholder} somecommand.py $leading_pholder + │ ^^^^^^^^^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: leading_pholder is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:126:37 + │ +126 │ ~{placeholder} somecommand.py $leading_pholder + │ ^^^^^^^^^^^^^^^^ + │ │ + │ SC2154[warning]: leading_pholder is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: occurs_after_multiline is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:132:7 + │ +132 │ $occurs_after_multiline + │ ^^^^^^^^^^^^^^^^^^^^^^^ + │ │ + │ SC2154[warning]: occurs_after_multiline is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Remove surrounding $() to avoid executing output (or use eval if intentional). + ┌─ tests/lints/shellcheck-warn/source.wdl:134:7 + │ +134 │ ╭ ╭ $(echo This is a +135 │ │ │ very long string that should be quoted) + │ ╰─│───────────────────────────────────────────────^ SC2091[warning]: Remove surrounding $() to avoid executing output (or use eval if intentional). + │ ╰───────────────────────────────────────────────' more info: https://www.shellcheck.net/wiki/SC2091 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Remove surrounding $() to avoid executing output (or use eval if intentional). + ┌─ tests/lints/shellcheck-warn/source.wdl:137:7 + │ +137 │ ╭ ╭ $(echo This is an +138 │ │ │ even longer very long string that should really +139 │ │ │ be quoted) + │ ╰─│──────────────────^ SC2091[warning]: Remove surrounding $() to avoid executing output (or use eval if intentional). + │ ╰──────────────────' more info: https://www.shellcheck.net/wiki/SC2091 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Remove surrounding $() to avoid executing output (or use eval if intentional). + ┌─ tests/lints/shellcheck-warn/source.wdl:141:7 + │ +141 │ ╭ ╭ $(echo This is an +142 │ │ │ even longer very long string that should really +143 │ │ │ really really really +144 │ │ │ ought to be quoted) + │ ╰─│───────────────────────────^ SC2091[warning]: Remove surrounding $() to avoid executing output (or use eval if intentional). + │ ╰───────────────────────────' more info: https://www.shellcheck.net/wiki/SC2091 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Remove surrounding $() to avoid executing output (or use eval if intentional). + ┌─ tests/lints/shellcheck-warn/source.wdl:146:7 + │ +146 │ ╭ ╭ $(echo this is a $lint146 that occurs in a / +147 │ │ │ multiline command / +148 │ │ │ with line breaks) + │ ╰─│─────────────────────────^ SC2091[warning]: Remove surrounding $() to avoid executing output (or use eval if intentional). + │ ╰─────────────────────────' more info: https://www.shellcheck.net/wiki/SC2091 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. + ┌─ tests/lints/shellcheck-warn/source.wdl:146:7 + │ +146 │ ╭ ╭ $(echo this is a $lint146 that occurs in a / +147 │ │ │ multiline command / +148 │ │ │ with line breaks) + │ ╰─│─────────────────────────^ SC2116[style]: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. + │ ╰─────────────────────────' more info: https://www.shellcheck.net/wiki/SC2116 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: Double quote to prevent globbing and word splitting. + ┌─ tests/lints/shellcheck-warn/source.wdl:146:24 + │ +146 │ $(echo this is a $lint146 that occurs in a / + │ ^^^^^^^^ + │ │ + │ SC2086[info]: Double quote to prevent globbing and word splitting. + │ more info: https://www.shellcheck.net/wiki/SC2086 + │ + = fix: address the diagnostic as recommended in the message + +note[ShellCheck]: lint146 is referenced but not assigned. + ┌─ tests/lints/shellcheck-warn/source.wdl:146:24 + │ +146 │ $(echo this is a $lint146 that occurs in a / + │ ^^^^^^^^ + │ │ + │ SC2154[warning]: lint146 is referenced but not assigned. + │ more info: https://www.shellcheck.net/wiki/SC2154 + │ + = fix: address the diagnostic as recommended in the message + diff --git a/wdl-lint/tests/lints/shellcheck-warn/source.wdl b/wdl-lint/tests/lints/shellcheck-warn/source.wdl new file mode 100644 index 000000000..66760195b --- /dev/null +++ b/wdl-lint/tests/lints/shellcheck-warn/source.wdl @@ -0,0 +1,154 @@ +#@ except: DescriptionMissing, RuntimeSectionKeys, MatchingParameterMeta, NoCurlyCommands + +## This is a test of having shellcheck warnings + +version 1.1 + +task test1 { + meta {} + + parameter_meta {} + + input { + Int placeholder + } + + command <<< + somecommand.py $line17 ~{placeholder} + somecommand.py ~{placeholder} $line18 + somecommand.py ~{placeholder}$line19 + + + + + + + + + + + somecommand.py $line30~{placeholder} + somecommand.py [ -f $line31 ] ~{placeholder} + >>> + + output {} + + runtime {} +} + +task test2 { + meta {} + + parameter_meta {} + + input { + Int placeholder + } + + command { + somecommand.py $line49 ~{placeholder} + somecommand.py ~{placeholder} $line50 + somecommand.py ~{placeholder}$line51 + somecommand.py $line52~{placeholder} + somecommand.py [ -f $bad_test ] ~{placeholder} + somecommand.py [ -f $trailing_space ] ~{placeholder} + } + + output {} + + runtime {} +} + +task test3 { + meta {} + + parameter_meta {} + + input { + Int placeholder + } + + command <<< # weird whitespace + somecommand.py $line72 ~{placeholder} + somecommand.py ~{placeholder} $line73 + somecommand.py ~{placeholder}$line74 + somecommand.py $line75~{placeholder} + ~{placeholder} $line76_trailing_pholder ~{placeholder} + ~{placeholder} somecommand.py $leading_pholder + >>> + + output {} + + runtime {} +} + +task test4 { + meta {} + + parameter_meta {} + + input { + Int placeholder + } + + command <<< + # other weird whitspace + somecommand.py $line96 ~{placeholder} + somecommand.py ~{placeholder} $line97 + somecommand.py ~{placeholder}$line98 + somecommand.py $line99~{placeholder} + ~{placeholder} $line100_trailing_pholder ~{placeholder} + ~{placeholder} somecommand.py $leading_pholder + >>> + + output {} + + runtime {} +} + +task test5 { + meta {} + + parameter_meta {} + + input { + Int placeholder + } + + command <<< weird stuff $firstlinelint + # other weird whitespace + somecommand.py $line120 ~{placeholder} + somecommand.py ~{placeholder} $line121 + somecommand.py ~{placeholder}$line122 + somecommand.py $line123~{placeholder} + ~{placeholder} $line124_trailing_pholder ~{placeholder} + ~{by + myself} + ~{placeholder} somecommand.py $leading_pholder + + ~{ + multiline + + placeholder + } + $occurs_after_multiline + + $(echo This is a + very long string that should be quoted) + + $(echo This is an + even longer very long string that should really + be quoted) + + $(echo This is an + even longer very long string that should really + really really really + ought to be quoted) + + $(echo this is a $lint146 that occurs in a \ + multiline command \ + with line breaks) + >>> + + output {} + + runtime {} +} diff --git a/wdl/CHANGELOG.md b/wdl/CHANGELOG.md index a02852ce7..d9fa7c583 100644 --- a/wdl/CHANGELOG.md +++ b/wdl/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Add `--shellcheck` flag to `wdl lint` subcommand to run shellcheck when linting ([#264](https://github.com/stjude-rust-labs/wdl/pull/264)) * Implemented the `wdl doc` subcommand for generating documentation (**currently in ALPHA testing**) ([#248](https://github.com/stjude-rust-labs/wdl/pull/248)). * Added an `--open` flag to `wdl doc` subcommand ([#269](https://github.com/stjude-rust-labs/wdl/pull/269)). * Added the `engine` module containing the implementation of `wdl-engine` ([#265](https://github.com/stjude-rust-labs/wdl/pull/265)). diff --git a/wdl/src/bin/wdl.rs b/wdl/src/bin/wdl.rs index 8de375cf0..66aae2102 100644 --- a/wdl/src/bin/wdl.rs +++ b/wdl/src/bin/wdl.rs @@ -50,6 +50,7 @@ use wdl_engine::local::LocalTaskExecutionBackend; use wdl_engine::v1::TaskEvaluator; use wdl_format::Formatter; use wdl_format::element::node::AstNodeFormatExt as _; +use wdl_lint::rules::ShellCheckRule; /// Emits the given diagnostics to the output stream. /// @@ -295,6 +296,9 @@ pub struct LintCommand { /// The path to the source WDL file. #[clap(value_name = "PATH")] pub path: PathBuf, + /// Enable shellcheck lints. + #[clap(long, action)] + pub shellcheck: bool, } impl LintCommand { @@ -314,6 +318,9 @@ impl LintCommand { let mut validator = Validator::default(); validator.add_visitor(LintVisitor::default()); + if self.shellcheck { + validator.add_visitor(ShellCheckRule); + } if let Err(diagnostics) = validator.validate(&document) { emit_diagnostics(&self.path.to_string_lossy(), &source, &diagnostics)?;