diff --git a/.github/renovate-tracked-deps.json b/.github/renovate-tracked-deps.json index 295c0200..c29d68fa 100644 --- a/.github/renovate-tracked-deps.json +++ b/.github/renovate-tracked-deps.json @@ -16,6 +16,10 @@ "packageName": "biomejs/biome", "datasource": "github-tags" }, + "checkstyle": { + "packageName": "checkstyle/checkstyle", + "datasource": "github-releases" + }, "editorconfig-checker": { "packageName": "editorconfig-checker/editorconfig-checker", "datasource": "github-releases" @@ -109,6 +113,7 @@ "aqua:jonwiggins/xmloxide", "aqua:owenlamont/ryl", "biome", + "checkstyle", "dotnet", "editorconfig-checker", "go", diff --git a/.github/renovate.json5 b/.github/renovate.json5 index e5f4c358..f83788ea 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -67,6 +67,7 @@ "aqua:jonwiggins/xmloxide", "aqua:owenlamont/ryl", "biome", + "checkstyle", "editorconfig-checker", "golangci-lint", "google-java-format", diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f5ae6c9a..53c4261f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,8 +25,8 @@ jobs: - name: Setup mise uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4.2.1 with: - version: v2026.7.12 - sha256: dad54e0b843908324282b8673f9c0ebc3a4da0c49ad2da309a49bfbc918ba180 + version: v2026.7.14 + sha256: fc96308f4fa085d7359892ac6351ededb35ecfabf1ddc34f5757bc755a2af8a6 # mise may activate an existing Rust toolchain without adding missing components. - name: Install Rust lint components diff --git a/.github/workflows/mise-registry.yml b/.github/workflows/mise-registry.yml index 181210b7..5ec3f835 100644 --- a/.github/workflows/mise-registry.yml +++ b/.github/workflows/mise-registry.yml @@ -23,8 +23,8 @@ jobs: - name: Setup mise uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4.2.1 with: - version: v2026.7.12 - sha256: dad54e0b843908324282b8673f9c0ebc3a4da0c49ad2da309a49bfbc918ba180 + version: v2026.7.14 + sha256: fc96308f4fa085d7359892ac6351ededb35ecfabf1ddc34f5757bc755a2af8a6 cache_key: "{{default}}-{{env.MISE_ENABLE_TOOLS}}" - name: Restore Rust cache uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 406d280d..086a894a 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -29,8 +29,8 @@ jobs: - name: Setup mise uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4.2.1 with: - version: v2026.7.12 - sha256: dad54e0b843908324282b8673f9c0ebc3a4da0c49ad2da309a49bfbc918ba180 + version: v2026.7.14 + sha256: fc96308f4fa085d7359892ac6351ededb35ecfabf1ddc34f5757bc755a2af8a6 cache_key: "{{default}}-{{env.MISE_ENABLE_TOOLS}}" - name: Create release metadata env: @@ -62,8 +62,8 @@ jobs: - name: Setup mise uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4.2.1 with: - version: v2026.7.12 - sha256: dad54e0b843908324282b8673f9c0ebc3a4da0c49ad2da309a49bfbc918ba180 + version: v2026.7.14 + sha256: fc96308f4fa085d7359892ac6351ededb35ecfabf1ddc34f5757bc755a2af8a6 cache_key: "{{default}}-{{env.MISE_ENABLE_TOOLS}}" - name: Update release PR env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 938a4c87..96d7bc74 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,35 +30,35 @@ jobs: include: - target: x86_64-unknown-linux-gnu runner: ubuntu-24.04 - mise_version: v2026.7.12 - mise_sha256: dad54e0b843908324282b8673f9c0ebc3a4da0c49ad2da309a49bfbc918ba180 + mise_version: v2026.7.14 + mise_sha256: fc96308f4fa085d7359892ac6351ededb35ecfabf1ddc34f5757bc755a2af8a6 - target: x86_64-unknown-linux-musl runner: ubuntu-24.04 build-tool: cross - mise_version: v2026.7.12 - mise_sha256: dad54e0b843908324282b8673f9c0ebc3a4da0c49ad2da309a49bfbc918ba180 + mise_version: v2026.7.14 + mise_sha256: fc96308f4fa085d7359892ac6351ededb35ecfabf1ddc34f5757bc755a2af8a6 - target: aarch64-unknown-linux-gnu runner: ubuntu-24.04 build-tool: cross - mise_version: v2026.7.12 - mise_sha256: dad54e0b843908324282b8673f9c0ebc3a4da0c49ad2da309a49bfbc918ba180 + mise_version: v2026.7.14 + mise_sha256: fc96308f4fa085d7359892ac6351ededb35ecfabf1ddc34f5757bc755a2af8a6 - target: aarch64-unknown-linux-musl runner: ubuntu-24.04 build-tool: cross - mise_version: v2026.7.12 - mise_sha256: dad54e0b843908324282b8673f9c0ebc3a4da0c49ad2da309a49bfbc918ba180 + mise_version: v2026.7.14 + mise_sha256: fc96308f4fa085d7359892ac6351ededb35ecfabf1ddc34f5757bc755a2af8a6 - target: x86_64-apple-darwin runner: macos-15-intel - mise_version: v2026.7.12 - mise_sha256: bbe1d2378dd42ff562b4fe688bf235e64c326117428f02c6a634dead29bd0945 + mise_version: v2026.7.14 + mise_sha256: 3a3cf40fd034f83bd5cdffd4d673d40b04a79d06affbd30e5fcc4f00ae0ac460 - target: aarch64-apple-darwin runner: macos-latest - mise_version: v2026.7.12 - mise_sha256: 4268f41491bcb89e750951041d415a847495639a00e894075c62057633d06982 + mise_version: v2026.7.14 + mise_sha256: 082262daa1cd73e22f71272c574afda560c4fcf39852bc18884eae9e13cd5f2c - target: x86_64-pc-windows-msvc runner: windows-latest - mise_version: v2026.7.12 - mise_sha256: 50781ee1c71d3b19de5299fd6dba917489cd24127da2aa69ec03cc18074284a7 + mise_version: v2026.7.14 + mise_sha256: acf83125cf0166701ffa661be72cbb26a852d31b22d2acc399a974f1d3812862 steps: - name: Checkout code diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ee7f1a6c..e4f994cd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,18 +17,18 @@ jobs: matrix: include: - os: ubuntu-24.04 - mise_version: v2026.7.12 - mise_sha256: dad54e0b843908324282b8673f9c0ebc3a4da0c49ad2da309a49bfbc918ba180 + mise_version: v2026.7.14 + mise_sha256: fc96308f4fa085d7359892ac6351ededb35ecfabf1ddc34f5757bc755a2af8a6 aube_node_linker: isolated aube_package_import_method: auto - os: macos-15 - mise_version: v2026.7.12 - mise_sha256: 4268f41491bcb89e750951041d415a847495639a00e894075c62057633d06982 + mise_version: v2026.7.14 + mise_sha256: 082262daa1cd73e22f71272c574afda560c4fcf39852bc18884eae9e13cd5f2c aube_node_linker: isolated aube_package_import_method: auto - os: windows-2025 - mise_version: v2026.7.12 - mise_sha256: 50781ee1c71d3b19de5299fd6dba917489cd24127da2aa69ec03cc18074284a7 + mise_version: v2026.7.14 + mise_sha256: acf83125cf0166701ffa661be72cbb26a852d31b22d2acc399a974f1d3812862 # Use a physical npm tree so mise-action can cache it reliably. aube_node_linker: hoisted aube_package_import_method: copy diff --git a/README.md b/README.md index fa06ae3b..41dbba64 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ For Flint contributor workflow and local testing tips, see | ----------------------- | ------------------------------------------------ | ---------------------------------------------------------- | | C# | — | [`dotnet-format`](docs/linters/dotnet-format.md) | | Go | [`golangci-lint`](docs/linters/golangci-lint.md) | [`gofmt`](docs/linters/gofmt.md) | -| Java | — | [`google-java-format`](docs/linters/google-java-format.md) | +| Java | [`checkstyle`](docs/linters/checkstyle.md) | [`google-java-format`](docs/linters/google-java-format.md) | | JavaScript / TypeScript | [`biome`](docs/linters/biome.md) | [`biome-format`](docs/linters/biome-format.md) | | Kotlin | [`ktlint`](docs/linters/ktlint.md) | [`ktlint`](docs/linters/ktlint.md) | | Python | [`ruff`](docs/linters/ruff.md) | [`ruff-format`](docs/linters/ruff-format.md) | diff --git a/default.json b/default.json index 12f71591..53cf192d 100644 --- a/default.json +++ b/default.json @@ -31,6 +31,7 @@ "aqua:jonwiggins/xmloxide", "aqua:owenlamont/ryl", "biome", + "checkstyle", "editorconfig-checker", "golangci-lint", "google-java-format", diff --git a/docs/linters.md b/docs/linters.md index 114df62c..99018115 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -14,7 +14,7 @@ page with its behavior, configuration, and examples. | ----------------------- | ------------------------------------------- | ----------------------------------------------------- | | C# | — | [`dotnet-format`](linters/dotnet-format.md) | | Go | [`golangci-lint`](linters/golangci-lint.md) | [`gofmt`](linters/gofmt.md) | -| Java | — | [`google-java-format`](linters/google-java-format.md) | +| Java | [`checkstyle`](linters/checkstyle.md) | [`google-java-format`](linters/google-java-format.md) | | JavaScript / TypeScript | [`biome`](linters/biome.md) | [`biome-format`](linters/biome-format.md) | | Kotlin | [`ktlint`](linters/ktlint.md) | [`ktlint`](linters/ktlint.md) | | Python | [`ruff`](linters/ruff.md) | [`ruff-format`](linters/ruff-format.md) | diff --git a/docs/linters/checkstyle.md b/docs/linters/checkstyle.md new file mode 100644 index 00000000..9ff2ce46 --- /dev/null +++ b/docs/linters/checkstyle.md @@ -0,0 +1,28 @@ +# `checkstyle` + + + + +| | | +| -------- | ------------------------------------------------------ | +| Project | [checkstyle](https://github.com/checkstyle/checkstyle) | +| Fix | no | +| Binary | `checkstyle` | +| Scope | [files](../linters.md#scope-files) | +| Patterns | `*.java` | +| Config | [`checkstyle.xml`](https://checkstyle.org/config.html) | + + + +`checkstyle` checks Java source against a repository-owned coding standard. It +is report-only; use a formatter such as +[`google-java-format`](google-java-format.md) for safe formatting fixes. + +Flint runs the standalone Checkstyle CLI against selected Java files. A Java +runtime must be available on `PATH` because Checkstyle is distributed as a JAR. +Flint resolves that JAR from the direct `checkstyle` entry in `mise.toml` and +invokes it with `java -jar` on every platform. + +The repository must provide `checkstyle.xml` at its root. A root-level +`checkstyle-suppressions.xml` is also supported through Checkstyle's standard +property default. Flint does not infer Maven or Gradle source roots. diff --git a/mise.toml b/mise.toml index dc50e700..ff7906bc 100644 --- a/mise.toml +++ b/mise.toml @@ -13,6 +13,7 @@ actionlint = "1.7.12" "aqua:jonwiggins/xmloxide" = "0.4.4" "aqua:owenlamont/ryl" = "0.21.0" biome = "2.5.5" +checkstyle = "13.8.0" editorconfig-checker = "3.8.0" golangci-lint = "2.12.2" google-java-format = "1.35.0" diff --git a/src/init/scaffold.rs b/src/init/scaffold.rs index 0813a2a6..933bcb0f 100644 --- a/src/init/scaffold.rs +++ b/src/init/scaffold.rs @@ -55,8 +55,8 @@ jobs: - name: Setup mise uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4.2.1 with: - version: v2026.7.12 - sha256: dad54e0b843908324282b8673f9c0ebc3a4da0c49ad2da309a49bfbc918ba180 + version: v2026.7.14 + sha256: fc96308f4fa085d7359892ac6351ededb35ecfabf1ddc34f5757bc755a2af8a6 {rust_steps} - name: Lint env: diff --git a/src/linters/mod.rs b/src/linters/mod.rs index d14b658e..d504bc08 100644 --- a/src/linters/mod.rs +++ b/src/linters/mod.rs @@ -21,13 +21,13 @@ pub use crate::registry::LinterOutput; /// - PE binary without extension → execute directly by full path /// - Everything else → route through `cmd.exe /C` to handle `.cmd` shims /// -/// Self-executing JARs (e.g. ktlint) cannot run via cmd.exe at all. -/// When `windows_java_jar` is true the binary is resolved to its full path -/// and invoked as `java -jar `. -pub fn spawn_command(argv: &[String], windows_java_jar: bool) -> tokio::process::Command { +/// Self-executing JARs (e.g. ktlint and Checkstyle) need to be invoked through +/// the JVM. When `java_jar` is true the resolved tool path is invoked as +/// `java -jar ` on every platform. +pub fn spawn_command(argv: &[String], java_jar: bool) -> tokio::process::Command { #[cfg(windows)] { - if windows_java_jar { + if java_jar { if let Some(path) = find_file_in_path(&argv[0]) { let mut cmd = tokio::process::Command::new("java"); cmd.arg("-jar").arg(path).args(&argv[1..]); @@ -44,7 +44,12 @@ pub fn spawn_command(argv: &[String], windows_java_jar: bool) -> tokio::process: } #[cfg(not(windows))] { - let _ = windows_java_jar; + if java_jar { + let jar = find_file_in_path(&argv[0]).unwrap_or_else(|| argv[0].clone().into()); + let mut cmd = tokio::process::Command::new("java"); + cmd.arg("-jar").arg(jar).args(&argv[1..]); + return cmd; + } let mut cmd = tokio::process::Command::new(&argv[0]); cmd.args(&argv[1..]); cmd @@ -79,7 +84,7 @@ fn find_pe_binary(binary: &str) -> Option { /// On Windows, return the full path of `binary` from PATH without inspecting /// its contents. Used for self-executing JARs where the caller already knows -/// the invocation style (i.e. `windows_java_jar` is set in the registry). +/// the invocation style (i.e. `java_jar` is set in the registry). #[cfg(windows)] fn find_file_in_path(binary: &str) -> Option { let path_var = std::env::var("PATH").ok()?; @@ -88,3 +93,12 @@ fn find_file_in_path(binary: &str) -> Option { candidate.is_file().then_some(candidate) }) } + +#[cfg(not(windows))] +fn find_file_in_path(binary: &str) -> Option { + let path_var = std::env::var("PATH").ok()?; + std::env::split_paths(&path_var).find_map(|dir| { + let candidate = dir.join(binary); + candidate.is_file().then_some(candidate) + }) +} diff --git a/src/main.rs b/src/main.rs index 353016a1..10ad1a1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1348,33 +1348,34 @@ rust = { version = "1.94.1", components = "clippy,rustfmt" } assert_eq!( table, - r#"NAME BINARY STATUS SPEED FIX DESCRIPTION PATTERNS -------------------------------------------------------------------------------------------------------------------------------------------------------- -flint-setup (built-in) active fast yes Keep Flint setup current and mise.toml lint tooling canonical mise.toml -shellcheck shellcheck active fast no Lint shell scripts for common mistakes *.sh *.bash *.bats -shfmt shfmt active fast yes Format shell scripts *.sh *.bash -rumdl rumdl active fast yes Lint Markdown files for style and consistency *.md -ryl ryl active fast yes Lint YAML files for style and consistency *.yml *.yaml -taplo taplo active fast yes Format TOML files *.toml -actionlint actionlint active fast no Lint GitHub Actions workflow files .github/workflows/*.yml .github/workflows/*.yaml -zizmor zizmor active fast yes Audit GitHub Actions workflows for security issues .github/workflows/*.yml .github/workflows/*.yaml -hadolint hadolint missing fast no Lint Dockerfiles Dockerfile Dockerfile.* *.dockerfile -xmllint xmllint missing fast no Validate XML files are well-formed *.xml -typos typos active fast yes Check for common spelling mistakes * -editorconfig-checker ec active fast no Check files comply with EditorConfig settings * -golangci-lint golangci-lint missing fast no Lint Go code; uses --new-from-rev to scope analysis to changed code *.go -ruff ruff active fast yes Lint Python code *.py -ruff-format ruff active fast yes Format Python code *.py -biome biome active fast yes Lint JS/TS/JSON files *.json *.jsonc *.js *.ts *.jsx *.tsx -biome-format biome active fast yes Format JS/TS/JSON files *.json *.jsonc *.js *.ts *.jsx *.tsx -cargo-clippy cargo-clippy active fast yes Lint Rust code; runs on all .rs files, not just changed *.rs -cargo-fmt rustfmt active fast yes Format Rust code; runs on all .rs files, not just changed *.rs -gofmt gofmt missing fast yes Format Go code *.go -google-java-format google-java-format missing fast yes Format Java code *.java -ktlint ktlint missing fast yes Lint and format Kotlin code *.kt *.kts -dotnet-format dotnet missing fast yes Format C# code *.cs + r#"NAME BINARY STATUS SPEED FIX DESCRIPTION PATTERNS +--------------------------------------------------------------------------------------------------------------------------------------------------------- +flint-setup (built-in) active fast yes Keep Flint setup current and mise.toml lint tooling canonical mise.toml +shellcheck shellcheck active fast no Lint shell scripts for common mistakes *.sh *.bash *.bats +shfmt shfmt active fast yes Format shell scripts *.sh *.bash +rumdl rumdl active fast yes Lint Markdown files for style and consistency *.md +ryl ryl active fast yes Lint YAML files for style and consistency *.yml *.yaml +taplo taplo active fast yes Format TOML files *.toml +actionlint actionlint active fast no Lint GitHub Actions workflow files .github/workflows/*.yml .github/workflows/*.yaml +zizmor zizmor active fast yes Audit GitHub Actions workflows for security issues .github/workflows/*.yml .github/workflows/*.yaml +hadolint hadolint missing fast no Lint Dockerfiles Dockerfile Dockerfile.* *.dockerfile +xmllint xmllint missing fast no Validate XML files are well-formed *.xml +typos typos active fast yes Check for common spelling mistakes * +editorconfig-checker ec active fast no Check files comply with EditorConfig settings * +golangci-lint golangci-lint missing fast no Lint Go code; uses --new-from-rev to scope analysis to changed code *.go +ruff ruff active fast yes Lint Python code *.py +ruff-format ruff active fast yes Format Python code *.py +biome biome active fast yes Lint JS/TS/JSON files *.json *.jsonc *.js *.ts *.jsx *.tsx +biome-format biome active fast yes Format JS/TS/JSON files *.json *.jsonc *.js *.ts *.jsx *.tsx +cargo-clippy cargo-clippy active fast yes Lint Rust code; runs on all .rs files, not just changed *.rs +cargo-fmt rustfmt active fast yes Format Rust code; runs on all .rs files, not just changed *.rs +gofmt gofmt missing fast yes Format Go code *.go +google-java-format google-java-format missing fast yes Format Java code *.java +checkstyle checkstyle missing fast no Check Java source against a repository-owned Checkstyle configuration *.java +ktlint ktlint missing fast yes Lint and format Kotlin code *.kt *.kts +dotnet-format dotnet missing fast yes Format C# code *.cs lychee lychee active fast no Check for broken links -renovate-deps renovate active adaptive yes Verify Renovate dependency snapshot is up to date renovate.json renovate.json5 .github/renovate.json .github/renovate.json5 .renovaterc .renovaterc.json .renovaterc.json5 +renovate-deps renovate active adaptive yes Verify Renovate dependency snapshot is up to date renovate.json renovate.json5 .github/renovate.json .github/renovate.json5 .renovaterc .renovaterc.json .renovaterc.json5 license-header (built-in) not configured fast no Check source files have the required license header "# ); diff --git a/src/registry/checks.rs b/src/registry/checks.rs index abbbafa0..9beb432e 100644 --- a/src/registry/checks.rs +++ b/src/registry/checks.rs @@ -26,6 +26,8 @@ const GOFMT_URL: &str = "https://pkg.go.dev/cmd/gofmt"; const GOLANGCI_LINT_URL: &str = "https://golangci-lint.run/"; const GOLANGCI_LINT_CONFIG_URL: &str = "https://golangci-lint.run/usage/configuration/"; const GOOGLE_JAVA_FORMAT_URL: &str = "https://github.com/google/google-java-format"; +const CHECKSTYLE_URL: &str = "https://github.com/checkstyle/checkstyle"; +const CHECKSTYLE_CONFIG_URL: &str = "https://checkstyle.org/config.html"; const HADOLINT_URL: &str = "https://github.com/hadolint/hadolint"; const HADOLINT_CONFIG_URL: &str = "https://github.com/hadolint/hadolint?tab=readme-ov-file#configure"; @@ -53,6 +55,9 @@ const XMLLINT_URL: &str = "https://github.com/jonwiggins/xmloxide"; const YAMLLINT_CONFIG_URL: &str = "https://yamllint.readthedocs.io/en/stable/configuration.html"; const RYL_URL: &str = "https://github.com/owenlamont/ryl"; +const CHECKSTYLE_BASELINE_TRIGGERS: &[ConfigFile] = + &[ConfigFile::project("checkstyle-suppressions.xml")]; + const SHELLCHECK_UNSUPPORTED_CONFIGS: &[ConfigFile] = &[ ConfigFile::config_dir("shellcheckrc"), ConfigFile::project("shellcheckrc"), @@ -596,6 +601,28 @@ fn check_google_java_format() -> Check { .lang() } +fn check_checkstyle() -> Check { + Check::files( + "checkstyle", + "checkstyle -c checkstyle.xml {FILES}", + &["*.java"], + ) + .java_jar() + .baseline_config(ConfigFile::project("checkstyle.xml")) + .baseline_triggers(CHECKSTYLE_BASELINE_TRIGGERS) + .failure_output_patterns(&["[WARN]", "[ERROR]"]) + .project_url(CHECKSTYLE_URL) + .config_doc_url(CHECKSTYLE_CONFIG_URL) + .overview( + OverviewSection::Languages, + "Java", + OverviewRole::Linter, + Some("Java coding standard"), + ) + .desc("Check Java source against a repository-owned Checkstyle configuration") + .lang() +} + fn check_ktlint() -> Check { Check::files( "ktlint", @@ -723,6 +750,7 @@ pub fn builtin() -> Vec { check_cargo_fmt(), check_gofmt(), check_google_java_format(), + check_checkstyle(), check_ktlint(), check_dotnet_format(), check_lychee(), diff --git a/src/registry/types.rs b/src/registry/types.rs index c0f053e1..566b5ffc 100644 --- a/src/registry/types.rs +++ b/src/registry/types.rs @@ -527,6 +527,8 @@ pub struct Check { pub status_hook: Option, /// Optional output normalizer used for non-verbose failing process runs. pub nonverbose_failure_output: Option, + /// Output markers that make an otherwise successful process invocation fail. + pub failure_output_patterns: &'static [&'static str], /// Optional hint appended when a known toolchain component is missing. pub missing_component_hint: Option, /// Additional config-like files that trigger an all-files baseline run when changed. @@ -550,9 +552,8 @@ pub struct Check { /// Toolchain keys stay above the `# Linters` header in `mise.toml` so they're /// visually separated from lint-only entries. pub toolchain: Option>, - /// On Windows, the binary is a self-executing JAR that cannot be run directly - /// or via cmd.exe — invoke as `java -jar ` instead. - pub windows_java_jar: bool, + /// How to invoke the binary when it is a self-executing JAR. + pub java_jar: Option, /// Extra generated workflow setup needed when this check is selected by `flint init`. pub workflow_setup: Option, pub fix_behavior: FixBehavior, @@ -569,6 +570,35 @@ pub struct Check { pub overviews: Vec, } +/// Controls where a self-executing JAR must be launched through `java -jar`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum JavaJarMode { + /// Use `java -jar` on every platform. + AllPlatforms, + /// Use `java -jar` only on Windows, where JAR shims cannot run directly. + WindowsOnly, +} + +impl JavaJarMode { + pub(crate) const fn enabled_on_current_platform(self) -> bool { + matches!(self, Self::AllPlatforms) || (cfg!(windows) && matches!(self, Self::WindowsOnly)) + } +} + +#[cfg(test)] +mod java_jar_mode_tests { + use super::JavaJarMode; + + #[test] + fn enables_only_on_configured_platforms() { + assert!(JavaJarMode::AllPlatforms.enabled_on_current_platform()); + assert_eq!( + JavaJarMode::WindowsOnly.enabled_on_current_platform(), + cfg!(windows) + ); + } +} + impl Check { pub fn has_fix(&self) -> bool { match &self.kind { @@ -669,6 +699,7 @@ impl Check { adaptive_relevance: None, status_hook: None, nonverbose_failure_output: None, + failure_output_patterns: &[], missing_component_hint: None, baseline_triggers: &[], is_formatter: false, @@ -684,7 +715,7 @@ impl Check { full_fix_cmd: "", scope, }, - windows_java_jar: false, + java_jar: None, workflow_setup: None, fix_behavior: FixBehavior::Definitive, fix_after: vec![], @@ -722,6 +753,7 @@ impl Check { adaptive_relevance: None, status_hook: None, nonverbose_failure_output: None, + failure_output_patterns: &[], missing_component_hint: None, baseline_triggers: &[], is_formatter: false, @@ -730,7 +762,7 @@ impl Check { activate_unconditionally: false, category: Category::Default, toolchain: None, - windows_java_jar: false, + java_jar: None, workflow_setup: None, fix_behavior: FixBehavior::Definitive, fix_after: vec![], @@ -798,10 +830,16 @@ impl Check { self } + /// Invoke this binary via `java -jar ` rather than directly. + /// Use for self-executing JARs (e.g. ktlint and Checkstyle). + pub fn java_jar(mut self) -> Self { + self.java_jar = Some(JavaJarMode::AllPlatforms); + self + } + /// On Windows, invoke this binary via `java -jar ` rather than directly. - /// Use for self-executing JARs (e.g. ktlint) that cmd.exe cannot run. pub fn windows_java_jar(mut self) -> Self { - self.windows_java_jar = true; + self.java_jar = Some(JavaJarMode::WindowsOnly); self } @@ -995,6 +1033,11 @@ impl Check { self } + pub fn failure_output_patterns(mut self, patterns: &'static [&'static str]) -> Self { + self.failure_output_patterns = patterns; + self + } + pub fn missing_component_hint( mut self, component: &'static str, diff --git a/src/runner.rs b/src/runner.rs index 954a29ca..dda9210d 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -47,6 +47,7 @@ struct InvocationOutputPolicy<'a> { env: &'a [(&'static str, &'static str)], nonverbose_filter_prefixes: &'a [&'static str], stderr_filter_prefixes: &'a [&'static str], + failure_output_patterns: &'a [&'static str], } /// A check with all inputs pre-resolved, ready to execute without borrowing @@ -56,10 +57,11 @@ enum PreparedCheck { name: String, argv_list: Vec>, tracked_files: Vec, - windows_java_jar: bool, + java_jar: bool, env: &'static [(&'static str, &'static str)], nonverbose_filter_prefixes: &'static [&'static str], stderr_filter_prefixes: &'static [&'static str], + failure_output_patterns: &'static [&'static str], nonverbose_failure_output: Option, missing_component_hint: Option, }, @@ -81,10 +83,11 @@ impl PreparedCheck { Self::Invocations { argv_list, tracked_files, - windows_java_jar, + java_jar, env, nonverbose_filter_prefixes, stderr_filter_prefixes, + failure_output_patterns, nonverbose_failure_output, missing_component_hint, .. @@ -97,7 +100,7 @@ impl PreparedCheck { let out = run_invocations( &name, &argv_list, - windows_java_jar, + java_jar, InvocationOutputPolicy { nonverbose: !verbose, env: if verbose { &[] } else { env }, @@ -107,6 +110,7 @@ impl PreparedCheck { nonverbose_filter_prefixes }, stderr_filter_prefixes: if verbose { &[] } else { stderr_filter_prefixes }, + failure_output_patterns, }, nonverbose_failure_output, missing_component_hint, @@ -274,10 +278,13 @@ fn prepare( name, argv_list, tracked_files, - windows_java_jar: check.windows_java_jar, + java_jar: check + .java_jar + .is_some_and(|mode| mode.enabled_on_current_platform()), env: check.env, nonverbose_filter_prefixes: check.nonverbose_filter_prefixes, stderr_filter_prefixes: check.stderr_filter_prefixes, + failure_output_patterns: check.failure_output_patterns, nonverbose_failure_output: check.nonverbose_failure_output, missing_component_hint: check.missing_component_hint, }) @@ -573,7 +580,7 @@ fn render_config_args(config_args: &[String]) -> String { async fn run_invocations( name: &str, invocations: &[Vec], - windows_java_jar: bool, + java_jar: bool, output_policy: InvocationOutputPolicy<'_>, nonverbose_failure_output: Option, missing_component_hint: Option, @@ -587,13 +594,16 @@ async fn run_invocations( if argv.is_empty() { continue; } - let mut cmd = crate::linters::spawn_command(argv, windows_java_jar); + let mut cmd = crate::linters::spawn_command(argv, java_jar); cmd.current_dir(root) .stdin(Stdio::null()) .envs(output_policy.env.iter().copied()); let result = cmd.output().await; match result { Ok(out) => { + let output_policy_failed = + output_contains_any(&out.stdout, output_policy.failure_output_patterns) + || output_contains_any(&out.stderr, output_policy.failure_output_patterns); if output_policy.nonverbose && !out.status.success() && let Some(normalize) = nonverbose_failure_output @@ -634,7 +644,7 @@ async fn run_invocations( combined_stderr.extend_from_slice(&stderr); } } - if !out.status.success() { + if !out.status.success() || output_policy_failed { all_ok = false; } } @@ -656,6 +666,17 @@ async fn run_invocations( } } +fn output_contains_any(output: &[u8], patterns: &[&str]) -> bool { + patterns + .iter() + .filter(|pattern| !pattern.is_empty()) + .any(|pattern| { + output + .windows(pattern.len()) + .any(|window| window == pattern.as_bytes()) + }) +} + fn filter_stderr_lines(stderr: &[u8], prefixes: &[&str]) -> Vec { let text = String::from_utf8_lossy(stderr); let mut out = String::new(); @@ -921,6 +942,7 @@ mod tests { adaptive_relevance: None, status_hook: None, nonverbose_failure_output: None, + failure_output_patterns: &[], missing_component_hint: None, baseline_triggers: &[], is_formatter: false, @@ -929,7 +951,7 @@ mod tests { activate_unconditionally: false, category: Category::Default, toolchain: None, - windows_java_jar: false, + java_jar: None, workflow_setup: None, fix_behavior: crate::registry::FixBehavior::Definitive, fix_after: vec![], @@ -960,6 +982,17 @@ mod tests { } } + #[test] + fn failure_output_patterns_match_stdout_and_stderr() { + assert!(output_contains_any(b"[WARN] violation\n", &["[WARN]"])); + assert!(output_contains_any( + b"error: [WARN] violation\n", + &["[WARN]"] + )); + assert!(!output_contains_any(b"clean\n", &["[WARN]"])); + assert!(!output_contains_any(b"anything\n", &[""])); + } + #[test] fn fix_order_follows_declared_dependencies() { let mut first = project_check(&[]); diff --git a/tests/cases/checkstyle/clean/files/Foo.java b/tests/cases/checkstyle/clean/files/Foo.java new file mode 100644 index 00000000..4e6a6de6 --- /dev/null +++ b/tests/cases/checkstyle/clean/files/Foo.java @@ -0,0 +1 @@ +class Foo {} diff --git a/tests/cases/checkstyle/clean/files/checkstyle.xml b/tests/cases/checkstyle/clean/files/checkstyle.xml new file mode 100644 index 00000000..aab0fd81 --- /dev/null +++ b/tests/cases/checkstyle/clean/files/checkstyle.xml @@ -0,0 +1 @@ + diff --git a/tests/cases/checkstyle/clean/files/mise.toml b/tests/cases/checkstyle/clean/files/mise.toml new file mode 100644 index 00000000..40b6fd3f --- /dev/null +++ b/tests/cases/checkstyle/clean/files/mise.toml @@ -0,0 +1,2 @@ +[tools] +checkstyle = "13.8.0" diff --git a/tests/cases/checkstyle/clean/test.toml b/tests/cases/checkstyle/clean/test.toml new file mode 100644 index 00000000..9d41c3ed --- /dev/null +++ b/tests/cases/checkstyle/clean/test.toml @@ -0,0 +1,15 @@ +[expected] +args = "run --full checkstyle" +exit = 0 + +[fake_bins] +checkstyle = ''' +#!/bin/sh +exit 0 +''' +java = ''' +#!/bin/sh +echo "Starting audit..." +echo "Audit done." >&2 +exit 0 +''' diff --git a/tests/cases/checkstyle/missing-config/files/Foo.java b/tests/cases/checkstyle/missing-config/files/Foo.java new file mode 100644 index 00000000..4e6a6de6 --- /dev/null +++ b/tests/cases/checkstyle/missing-config/files/Foo.java @@ -0,0 +1 @@ +class Foo {} diff --git a/tests/cases/checkstyle/missing-config/files/mise.toml b/tests/cases/checkstyle/missing-config/files/mise.toml new file mode 100644 index 00000000..40b6fd3f --- /dev/null +++ b/tests/cases/checkstyle/missing-config/files/mise.toml @@ -0,0 +1,2 @@ +[tools] +checkstyle = "13.8.0" diff --git a/tests/cases/checkstyle/missing-config/test.toml b/tests/cases/checkstyle/missing-config/test.toml new file mode 100644 index 00000000..2878b1df --- /dev/null +++ b/tests/cases/checkstyle/missing-config/test.toml @@ -0,0 +1,21 @@ +[expected] +args = "run --full checkstyle" +exit = 1 +stderr = ''' +[checkstyle] +Could not find config XML file 'checkstyle.xml'. + +flint: 1 check failed (checkstyle) +💡 Try `flint run --fix` to auto-fix lint issues, then re-run `flint run` to verify. +''' + +[fake_bins] +checkstyle = ''' +#!/bin/sh +exit 0 +''' +java = ''' +#!/bin/sh +printf "Could not find config XML file 'checkstyle.xml'.\\n" +exit 1 +''' diff --git a/tests/cases/checkstyle/warning/files/Foo.java b/tests/cases/checkstyle/warning/files/Foo.java new file mode 100644 index 00000000..4e6a6de6 --- /dev/null +++ b/tests/cases/checkstyle/warning/files/Foo.java @@ -0,0 +1 @@ +class Foo {} diff --git a/tests/cases/checkstyle/warning/files/checkstyle.xml b/tests/cases/checkstyle/warning/files/checkstyle.xml new file mode 100644 index 00000000..aab0fd81 --- /dev/null +++ b/tests/cases/checkstyle/warning/files/checkstyle.xml @@ -0,0 +1 @@ + diff --git a/tests/cases/checkstyle/warning/files/mise.toml b/tests/cases/checkstyle/warning/files/mise.toml new file mode 100644 index 00000000..40b6fd3f --- /dev/null +++ b/tests/cases/checkstyle/warning/files/mise.toml @@ -0,0 +1,2 @@ +[tools] +checkstyle = "13.8.0" diff --git a/tests/cases/checkstyle/warning/test.toml b/tests/cases/checkstyle/warning/test.toml new file mode 100644 index 00000000..2d2cd7f8 --- /dev/null +++ b/tests/cases/checkstyle/warning/test.toml @@ -0,0 +1,21 @@ +[expected] +args = "run --full checkstyle" +exit = 1 +stderr = ''' +[checkstyle] +[WARN] Foo.java:1: warning + +flint: 1 check failed (checkstyle) +💡 Try `flint run --fix` to auto-fix lint issues, then re-run `flint run` to verify. +''' + +[fake_bins] +checkstyle = ''' +#!/bin/sh +exit 0 +''' +java = ''' +#!/bin/sh +printf '[WARN] Foo.java:1: warning\n' +exit 0 +'''