diff --git a/README.md b/README.md index 8e645768..2e0141e7 100644 --- a/README.md +++ b/README.md @@ -21,38 +21,39 @@ Features: New tools are being added frequently, so check this page again! -| Language | Formatter | Linter(s) | -| ---------------------- | --------------------- | -------------------- | -| C / C++ | [clang-format] | [clang-tidy] | -| Cuda | [clang-format] | | -| CSS, Less, Sass | [Prettier] | [Stylelint] | -| Go | [gofmt] or [gofumpt] | | -| GraphQL | [Prettier] | | -| HCL (Hashicorp Config) | [terraform] fmt | | -| HTML | [Prettier] | | -| JSON | [Prettier] | | -| Java | [google-java-format] | [pmd] , [Checkstyle] | -| JavaScript | [Prettier] | [ESLint] | -| Jsonnet | [jsonnetfmt] | | -| Kotlin | [ktfmt] | [ktlint] | -| Markdown | [Prettier] | [Vale] | -| Protocol Buffer | [buf] | [buf lint] | -| Python | [ruff] | [flake8], [ruff] | -| Rust | [rustfmt] | | -| SQL | [prettier-plugin-sql] | | -| Scala | [scalafmt] | | -| Shell | [shfmt] | [shellcheck] | -| Starlark | [Buildifier] | | -| Swift | [SwiftFormat] (1) | | -| TSX | [Prettier] | [ESLint] | -| TypeScript | [Prettier] | [ESLint] | -| YAML | [yamlfmt] | | +| Language | Formatter | Linter(s) | +| ---------------------- | --------------------- |----------------------------------| +| C / C++ | [clang-format] | [clang-tidy] | +| Cuda | [clang-format] | | +| CSS, Less, Sass | [Prettier] | [Stylelint] | +| Go | [gofmt] or [gofumpt] | | +| GraphQL | [Prettier] | | +| HCL (Hashicorp Config) | [terraform] fmt | | +| HTML | [Prettier] | | +| JSON | [Prettier] | | +| Java | [google-java-format] | [pmd] , [Checkstyle], [Spotbugs] | +| JavaScript | [Prettier] | [ESLint] | +| Jsonnet | [jsonnetfmt] | | +| Kotlin | [ktfmt] | [ktlint] | +| Markdown | [Prettier] | [Vale] | +| Protocol Buffer | [buf] | [buf lint] | +| Python | [ruff] | [flake8], [ruff] | +| Rust | [rustfmt] | | +| SQL | [prettier-plugin-sql] | | +| Scala | [scalafmt] | | +| Shell | [shfmt] | [shellcheck] | +| Starlark | [Buildifier] | | +| Swift | [SwiftFormat] (1) | | +| TSX | [Prettier] | [ESLint] | +| TypeScript | [Prettier] | [ESLint] | +| YAML | [yamlfmt] | | [prettier]: https://prettier.io [google-java-format]: https://github.com/google/google-java-format [flake8]: https://flake8.pycqa.org/en/latest/index.html [pmd]: https://docs.pmd-code.org/latest/index.html [checkstyle]: https://checkstyle.sourceforge.io/cmdline.html +[spotbugs]: https://spotbugs.github.io/ [buf lint]: https://buf.build/docs/lint/overview [eslint]: https://eslint.org/ [swiftformat]: https://github.com/nicklockwood/SwiftFormat diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index 0f7cc148..0c0d296f 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -32,6 +32,11 @@ stardoc_with_diff_test( bzl_library_target = "//lint:checkstyle", ) +stardoc_with_diff_test( + name = "spotbugs", + bzl_library_target = "//lint:spotbugs", +) + stardoc_with_diff_test( name = "format", bzl_library_target = "//format:defs", diff --git a/docs/spotbugs.md b/docs/spotbugs.md new file mode 100644 index 00000000..0a1777a6 --- /dev/null +++ b/docs/spotbugs.md @@ -0,0 +1,98 @@ + + +API for declaring a spotbugs lint aspect that visits java_library and java_binary rules. + +Typical usage: + +First, call the `fetch_spotbugs` helper in `WORKSPACE` to download the jar file. +Alternatively you could use whatever you prefer for managing Java dependencies, such as a Maven integration rule. + +Next, declare a binary target for it, typically in `tools/lint/BUILD.bazel`: + +```starlark +java_binary( + name = "spotbugs", + main_class = "edu.umd.cs.findbugs.LaunchAppropriateUI", + runtime_deps = [ + "@spotbugs//:jar", + ], +) +``` + +Finally, declare an aspect for it, typically in `tools/lint/linters.bzl`: + +```starlark +load("@aspect_rules_lint//lint:spotbugs.bzl", "lint_spotbugs_aspect") + +spotbugs = lint_spotbugs_aspect( + binary = "@@//tools/lint:spotbugs", + exclude_filter = "@@//:spotbugs-exclude.xml", +) + +``` + + + +## fetch_spotbugs + +
+load("@aspect_rules_lint//lint:spotbugs.bzl", "fetch_spotbugs")
+
+fetch_spotbugs()
+
+ + + + + + + +## lint_spotbugs_aspect + +
+load("@aspect_rules_lint//lint:spotbugs.bzl", "lint_spotbugs_aspect")
+
+lint_spotbugs_aspect(binary, exclude_filter, rule_kinds)
+
+ + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| binary |

-

| none | +| exclude_filter |

-

| none | +| rule_kinds |

-

| `["java_library", "java_binary"]` | + + + + +## spotbugs_action + +
+load("@aspect_rules_lint//lint:spotbugs.bzl", "spotbugs_action")
+
+spotbugs_action(ctx, executable, srcs, exclude_filter, stdout, exit_code, options)
+
+ +Run Spotbugs as an action under Bazel. + +Based on https://spotbugs.readthedocs.io/en/latest/index.html + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| ctx | Bazel Rule or Aspect evaluation context | none | +| executable | label of the the Spotbugs program | none | +| srcs | jar to be linted | none | +| exclude_filter | label of the spotbugs-exclude.xml file | none | +| stdout | output file to generate | none | +| exit_code | output file to write the exit code. If None, then fail the build when Spotbugs exits non-zero. | `None` | +| options | additional command-line options, see https://spotbugs.readthedocs.io/en/latest/running.html#command-line-options | `[]` | + + diff --git a/example/.aspect/cli/config.yaml b/example/.aspect/cli/config.yaml index 2fc0bf5c..a13ca939 100644 --- a/example/.aspect/cli/config.yaml +++ b/example/.aspect/cli/config.yaml @@ -10,3 +10,4 @@ lint: - //tools/lint:linters.bzl%vale - //tools/lint:linters.bzl%checkstyle - //tools/lint:linters.bzl%clang_tidy + - //tools/lint:linters.bzl%spotbugs diff --git a/example/.bazelrc b/example/.bazelrc index 400bca34..7201ba71 100644 --- a/example/.bazelrc +++ b/example/.bazelrc @@ -1,9 +1,33 @@ # Automatically apply --config=linux, --config=windows etc common --enable_platform_specific_config -# Don't depend on a JAVA_HOME pointing at a system JDK +# Aspect recommended Bazel flags when using rules_java and rules_jvm_external + +# Pin java versions to desired language level +# See https://bazel.build/docs/bazel-and-java#java-versions +# and https://en.wikipedia.org/wiki/Java_version_history + +# What version of Java are the source files in this repo? +# See https://bazel.build/docs/user-manual#java-language-version +common --java_language_version=17 + +# The Java language version used to build tools that are executed during a build +# See https://bazel.build/docs/user-manual#tool-java-language-version +common --tool_java_language_version=17 + +# The version of JVM to use to execute the code and run the tests. +# NB: The default value is local_jdk which is non-hermetic. +# See https://bazel.build/docs/user-manual#java-runtime-version +common --java_runtime_version=remotejdk_17 + +# The version of JVM used to execute tools that are needed during a build. +# See https://bazel.build/docs/user-manual#tool-java-runtime-version +common --tool_java_runtime_version=remotejdk_17 + +# Repository rules, such as rules_jvm_external: put Bazel's JDK on the path. +# Avoids non-hermeticity from dependency on a JAVA_HOME pointing at a system JDK # see https://github.com/bazelbuild/rules_jvm_external/issues/445 -build --repo_env=JAVA_HOME=../bazel_tools/jdk +common --repo_env=JAVA_HOME=../bazel_tools/jdk common --incompatible_enable_proto_toolchain_resolution common --@aspect_rules_ts//ts:skipLibCheck=always diff --git a/example/BUILD.bazel b/example/BUILD.bazel index ec13673e..b8d151ee 100644 --- a/example/BUILD.bazel +++ b/example/BUILD.bazel @@ -25,6 +25,7 @@ exports_files( ".editorconfig", "ktlint-baseline.xml", ".clang-tidy", + "spotbugs-exclude.xml", ], visibility = ["//visibility:public"], ) diff --git a/example/MODULE.bazel b/example/MODULE.bazel index 817a2b71..eb27adaf 100644 --- a/example/MODULE.bazel +++ b/example/MODULE.bazel @@ -9,8 +9,8 @@ bazel_dep(name = "rules_buf", version = "0.3.0") bazel_dep(name = "bazel_skylib", version = "1.4.2") bazel_dep(name = "toolchains_llvm", version = "0.10.3") bazel_dep(name = "toolchains_protoc", version = "0.3.0") -bazel_dep(name = "rules_java", version = "5.5.0") -bazel_dep(name = "rules_jvm_external", version = "4.5") +bazel_dep(name = "rules_java", version = "8.5.0") +bazel_dep(name = "rules_jvm_external", version = "6.5") bazel_dep(name = "rules_go", version = "0.42.0", repo_name = "io_bazel_rules_go") bazel_dep(name = "rules_proto", version = "6.0.0") bazel_dep(name = "rules_python", version = "0.26.0") diff --git a/example/WORKSPACE.bazel b/example/WORKSPACE.bazel index 605aade4..1ee606ee 100644 --- a/example/WORKSPACE.bazel +++ b/example/WORKSPACE.bazel @@ -318,6 +318,10 @@ load("@aspect_rules_lint//lint:ktlint.bzl", "fetch_ktlint") fetch_ktlint() +load("@aspect_rules_lint//lint:spotbugs.bzl", "fetch_spotbugs") + +fetch_spotbugs() + ######################## # Optional: multitool provides defaults for some tools such as yamlfmt # If you do not set up multitool, you must provide these tools yourself diff --git a/example/WORKSPACE.bzlmod b/example/WORKSPACE.bzlmod index 840a1e98..81fbceb3 100644 --- a/example/WORKSPACE.bzlmod +++ b/example/WORKSPACE.bzlmod @@ -31,3 +31,7 @@ fetch_vale() load("@aspect_rules_lint//lint:ktlint.bzl", "fetch_ktlint") fetch_ktlint() + +load("@aspect_rules_lint//lint:spotbugs.bzl", "fetch_spotbugs") + +fetch_spotbugs() diff --git a/example/lint.sh b/example/lint.sh index 036d47f4..e552d63d 100755 --- a/example/lint.sh +++ b/example/lint.sh @@ -37,7 +37,7 @@ if [ $machine == "Windows" ]; then # avoid missing linters on windows platform args=("--aspects=$(echo //tools/lint:linters.bzl%{flake8,pmd,ruff,vale,clang_tidy} | tr ' ' ',')") else - args=("--aspects=$(echo //tools/lint:linters.bzl%{buf,eslint,flake8,ktlint,pmd,ruff,shellcheck,stylelint,vale,clang_tidy} | tr ' ' ',')") + args=("--aspects=$(echo //tools/lint:linters.bzl%{buf,eslint,flake8,ktlint,pmd,ruff,shellcheck,stylelint,vale,clang_tidy,spotbugs} | tr ' ' ',')") fi # NB: perhaps --remote_download_toplevel is needed as well with remote execution? diff --git a/example/spotbugs-exclude.xml b/example/spotbugs-exclude.xml new file mode 100644 index 00000000..e8f84e3d --- /dev/null +++ b/example/spotbugs-exclude.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/example/src/Foo.java b/example/src/Foo.java index 1b03b370..845fefaa 100644 --- a/example/src/Foo.java +++ b/example/src/Foo.java @@ -4,7 +4,31 @@ // "category/java/errorprone.xml/FinalizeOverloaded" // src/equals_null.java:6: FinalizeOverloaded: Finalize methods should not be overloaded // Error: bazel exited with exit code: 4 + +//spotbugs result +//M B OS: Foo.readFile() may fail to close stream At Foo.java:[line 18] +//H D DLS: Dead store to $L1 in Foo.readFile() At Foo.java:[line 18] +//M X OBL: Foo.readFile() may fail to clean up java.io.InputStream Obligation to clean up resource created at Foo.java:[line 18] is not discharged public class Foo { - // this is confusing and probably a bug - protected void finalize(int a) {} + + // SpotBugs violation: Logical errors (NP_NULL_ON_SOME_PATH + DLS_DEAD_STORE) + public void someMethod(String str) { + if (str.equals("test")) { // Possible NPE if str is null + System.out.println("Valid string"); + } + + int a = 5; + a = 10; // The assignment to 5 is dead (overwritten with 10) + System.out.println(a); + } + + // SpotBugs violation: Resource leak (RCN_RESOURCE_LEAK) + public void readFile() { + try { + java.io.FileInputStream fis = new java.io.FileInputStream("somefile.txt"); + // FileInputStream not closed, causing resource leak + } catch (java.io.IOException e) { + e.printStackTrace(); + } + } } diff --git a/example/tools/lint/BUILD.bazel b/example/tools/lint/BUILD.bazel index 86f3ebe7..8741fb0e 100644 --- a/example/tools/lint/BUILD.bazel +++ b/example/tools/lint/BUILD.bazel @@ -46,6 +46,21 @@ java_binary( runtime_deps = ["@com_puppycrawl_tools_checkstyle//jar"], ) +java_binary( + name = "spotbugs", + jvm_flags = [ + "--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + ], + main_class = "edu.umd.cs.findbugs.LaunchAppropriateUI", + runtime_deps = [ + "@spotbugs//:jar", + ], +) + native_binary( name = "vale", src = select( diff --git a/example/tools/lint/linters.bzl b/example/tools/lint/linters.bzl index 49ee67d2..a0cbd1c4 100644 --- a/example/tools/lint/linters.bzl +++ b/example/tools/lint/linters.bzl @@ -10,6 +10,7 @@ load("@aspect_rules_lint//lint:lint_test.bzl", "lint_test") load("@aspect_rules_lint//lint:pmd.bzl", "lint_pmd_aspect") load("@aspect_rules_lint//lint:ruff.bzl", "lint_ruff_aspect") load("@aspect_rules_lint//lint:shellcheck.bzl", "lint_shellcheck_aspect") +load("@aspect_rules_lint//lint:spotbugs.bzl", "lint_spotbugs_aspect") load("@aspect_rules_lint//lint:stylelint.bzl", "lint_stylelint_aspect") load("@aspect_rules_lint//lint:vale.bzl", "lint_vale_aspect") @@ -110,3 +111,10 @@ clang_tidy_global_config = lint_clang_tidy_aspect( angle_includes_are_system = False, verbose = False, ) + +spotbugs = lint_spotbugs_aspect( + binary = "@@//tools/lint:spotbugs", + exclude_filter = "@@//:spotbugs-exclude.xml", +) + +spotbugs_test = lint_test(aspect = spotbugs) diff --git a/lint/BUILD.bazel b/lint/BUILD.bazel index 451e24a6..61e15f91 100644 --- a/lint/BUILD.bazel +++ b/lint/BUILD.bazel @@ -179,6 +179,15 @@ bzl_library( ], ) +bzl_library( + name = "spotbugs", + srcs = ["spotbugs.bzl"], + visibility = ["//visibility:public"], + deps = _BAZEL_TOOLS + [ + "//lint/private:lint_aspect", + ], +) + bzl_library( name = "ruff", srcs = ["ruff.bzl"], diff --git a/lint/spotbugs.bzl b/lint/spotbugs.bzl new file mode 100644 index 00000000..790e0829 --- /dev/null +++ b/lint/spotbugs.bzl @@ -0,0 +1,142 @@ +"""API for declaring a spotbugs lint aspect that visits java_library and java_binary rules. + +Typical usage: + +First, call the `fetch_spotbugs` helper in `WORKSPACE` to download the jar file. +Alternatively you could use whatever you prefer for managing Java dependencies, such as a Maven integration rule. + +Next, declare a binary target for it, typically in `tools/lint/BUILD.bazel`: + +```starlark +java_binary( + name = "spotbugs", + main_class = "edu.umd.cs.findbugs.LaunchAppropriateUI", + runtime_deps = [ + "@spotbugs//:jar", + ], +) +``` + +Finally, declare an aspect for it, typically in `tools/lint/linters.bzl`: + +```starlark +load("@aspect_rules_lint//lint:spotbugs.bzl", "lint_spotbugs_aspect") + +spotbugs = lint_spotbugs_aspect( + binary = "@@//tools/lint:spotbugs", + exclude_filter = "@@//:spotbugs-exclude.xml", +) + +``` +""" + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_jar") +load("//lint/private:lint_aspect.bzl", "LintOptionsInfo", "filter_srcs", "noop_lint_action", "output_files", "should_visit") + +_MNEMONIC = "AspectRulesLintCSpotbugs" + +def spotbugs_action(ctx, executable, srcs, exclude_filter, stdout, exit_code = None, options = []): + """Run Spotbugs as an action under Bazel. + + Based on https://spotbugs.readthedocs.io/en/latest/index.html + + Args: + ctx: Bazel Rule or Aspect evaluation context + executable: label of the the Spotbugs program + srcs: jar to be linted + exclude_filter: label of the spotbugs-exclude.xml file + stdout: output file to generate + exit_code: output file to write the exit code. + If None, then fail the build when Spotbugs exits non-zero. + options: additional command-line options, see https://spotbugs.readthedocs.io/en/latest/running.html#command-line-options + """ + inputs = srcs + [exclude_filter] + outputs = [stdout] + args = ctx.actions.args() + args.add_all(options) + + if exclude_filter: + args.add_all(["-exclude", exclude_filter.path]) + + src_args = ctx.actions.args() + src_args.add_all(srcs) + args.add_all(["-exclude", exclude_filter.path]) + + if exit_code: + command = "{SPOTBUGS} $@ >{stdout}; echo $? > " + exit_code.path + outputs.append(exit_code) + else: + # Create empty stdout file on success, as Bazel expects one + command = "{SPOTBUGS} $@ && touch {stdout}" + ctx.actions.run_shell( + inputs = inputs, + outputs = outputs, + command = command.format(SPOTBUGS = executable.path, stdout = stdout.path), + arguments = [args, src_args], + mnemonic = _MNEMONIC, + tools = [executable], + progress_message = "Linting %{label} with Spotbugs", + ) + +# buildifier: disable=function-docstrings +def _spotbugs_aspect_impl(target, ctx): + if not should_visit(ctx.rule, ctx.attr._rule_kinds): + return [] + + print("Print target %s", target.label) + print("JavaInfo %s", target[JavaInfo]) + print("OutputGroupInfo %s", target[OutputGroupInfo]) + + files_to_lint = [jar.class_jar for jar in target[JavaInfo].outputs.jars] + print("Files to lint %s", files_to_lint) + outputs, info = output_files(_MNEMONIC, target, ctx) + if len(files_to_lint) == 0: + noop_lint_action(ctx, outputs) + return [info] + format_options = [] # to define + spotbugs_action(ctx, ctx.executable._spotbugs, files_to_lint, ctx.file._exclude_filter, outputs.human.out, outputs.human.exit_code, format_options) + spotbugs_action(ctx, ctx.executable._spotbugs, files_to_lint, ctx.file._exclude_filter, outputs.machine.out, outputs.machine.exit_code, format_options) + return [info] + +def lint_spotbugs_aspect(binary, exclude_filter, rule_kinds = ["java_library", "java_binary"]): + return aspect( + implementation = _spotbugs_aspect_impl, + # Edges we need to walk up the graph from the selected targets. + # Needed for linters that need semantic information like transitive type declarations. + attr_aspects = ["deps"], + attrs = { + "_options": attr.label( + default = "//lint:options", + providers = [LintOptionsInfo], + ), + "_spotbugs": attr.label( + default = binary, + executable = True, + cfg = "exec", + ), + "_exclude_filter": attr.label( + doc = "Report all bug instances except those matching the filter specified by this filter file", + allow_single_file = True, + default = exclude_filter, + ), + "_rule_kinds": attr.string_list( + default = rule_kinds, + ), + }, + ) + +def fetch_spotbugs(): + http_archive( + name = "spotbugs", + urls = ["https://github.com/spotbugs/spotbugs/releases/download/4.8.6/spotbugs-4.8.6.zip"], + strip_prefix = "spotbugs-4.8.6", + build_file_content = """ +java_import( + name = "jar", + jars = [ + "lib/spotbugs.jar", + ], + visibility = ["//visibility:public"], +) + """, + )