diff --git a/README.md b/README.md index 4d9c46d2..d14c32ec 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Linters which are not language-specific: | Markdown | [Prettier] | [Vale] | | Protocol Buffer | [buf] | [buf lint] | | Python | [ruff] | [flake8], [pylint], [ruff] | +| Ruby | | [RuboCop] | | Rust | [rustfmt] | | | SQL | [prettier-plugin-sql] | | | Scala | [scalafmt] | | @@ -83,6 +84,7 @@ Linters which are not language-specific: [gofumpt]: https://github.com/mvdan/gofumpt [jsonnetfmt]: https://github.com/google/go-jsonnet [scalafmt]: https://scalameta.org/scalafmt +[rubocop]: https://docs.rubocop.org/ [ruff]: https://docs.astral.sh/ruff/ [pylint]: https://pylint.readthedocs.io/en/stable/ [shellcheck]: https://www.shellcheck.net/ diff --git a/example/.bazelrc b/example/.bazelrc index a0aec661..a2b5a20c 100644 --- a/example/.bazelrc +++ b/example/.bazelrc @@ -1,5 +1,5 @@ import %workspace%/../tools/preset.bazelrc -import %workspace%/tools/java17.bazelrc +import %workspace%/tools/java21.bazelrc # Automatically apply --config=linux, --config=windows etc common --enable_platform_specific_config diff --git a/example/.rubocop.yml b/example/.rubocop.yml new file mode 100644 index 00000000..d336994a --- /dev/null +++ b/example/.rubocop.yml @@ -0,0 +1,24 @@ +# RuboCop configuration for rules_lint example + +AllCops: + NewCops: enable + TargetRubyVersion: 3.0 + +# Configure line length +Layout/LineLength: + Max: 100 + +# Enable some common cops for demonstration +Style/StringLiterals: + Enabled: true + EnforcedStyle: single_quotes + +Style/TrailingCommaInArrayLiteral: + Enabled: true + EnforcedStyleForMultiline: consistent_comma + +Lint/UselessAssignment: + Enabled: true + +Style/DoubleNegation: + Enabled: true diff --git a/example/.ruby-version b/example/.ruby-version new file mode 100644 index 00000000..7f84f9e1 --- /dev/null +++ b/example/.ruby-version @@ -0,0 +1 @@ +jruby-10.0.2.0 diff --git a/example/BUILD.bazel b/example/BUILD.bazel index b6cd88b2..30360a42 100644 --- a/example/BUILD.bazel +++ b/example/BUILD.bazel @@ -20,6 +20,7 @@ exports_files( "checkstyle.xml", "checkstyle-suppressions.xml", ".ruff.toml", + ".rubocop.yml", ".shellcheckrc", ".scalafmt.conf", ".swcrc", diff --git a/example/Gemfile b/example/Gemfile new file mode 100644 index 00000000..d4790124 --- /dev/null +++ b/example/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'rubocop', '~> 1.50' diff --git a/example/Gemfile.lock b/example/Gemfile.lock new file mode 100644 index 00000000..c48e1460 --- /dev/null +++ b/example/Gemfile.lock @@ -0,0 +1,47 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.3) + json (2.15.1) + json (2.15.1-java) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + parallel (1.27.0) + parser (3.3.9.0) + ast (~> 2.4.1) + racc + prism (1.5.2) + racc (1.8.1) + racc (1.8.1-java) + rainbow (3.1.1) + regexp_parser (2.11.3) + rubocop (1.81.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.47.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + ruby-progressbar (1.13.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + +PLATFORMS + arm64-darwin-24 + ruby + universal-java + universal-java-21 + +DEPENDENCIES + rubocop (~> 1.50) + +BUNDLED WITH + 2.6.9 diff --git a/example/MODULE.bazel b/example/MODULE.bazel index 85022bc8..9e3ccfa3 100644 --- a/example/MODULE.bazel +++ b/example/MODULE.bazel @@ -19,6 +19,7 @@ bazel_dep(name = "rules_go", version = "0.52.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") bazel_dep(name = "rules_rust", version = "0.50.1") +bazel_dep(name = "rules_ruby", version = "0.21.1") bazel_dep(name = "buildifier_prebuilt", version = "6.3.3") bazel_dep(name = "platforms", version = "0.0.8") bazel_dep(name = "rules_kotlin", version = "1.9.0") @@ -128,3 +129,36 @@ rust.toolchain( edition = "2021", versions = ["1.75.0"], ) + +ruby = use_extension("@rules_ruby//ruby:extensions.bzl", "ruby") +ruby.toolchain( + name = "ruby", + version_file = "//:.ruby-version", +) +ruby.bundle_fetch( + name = "bundle", + gem_checksums = { + "ast-2.4.3": "954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383", + "json-2.15.1": "b1c1b2e7c116eb1903e0ce0c374783e6ead8747a0f9eca132d274018ebb80b89", + "json-2.15.1-java": "a6185eebe724a6937f60729e4998276d6b3de3ecc35be34f8e47c1eb40903ecf", + "language_server-protocol-3.17.0.5": "fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc", + "lint_roller-1.1.0": "2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87", + "parallel-1.27.0": "4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130", + "parser-3.3.9.0": "94d6929354b1a6e3e1f89d79d4d302cc8f5aa814431a6c9c7e0623335d7687f2", + "prism-1.5.2": "192741663a55af1ac1b987caa1092deb666e4ff46a30c5064ad5456acd05df1d", + "racc-1.8.1": "4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f", + "racc-1.8.1-java": "54f2e6d1e1b91c154013277d986f52a90e5ececbe91465d29172e49342732b98", + "rainbow-3.1.1": "039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a", + "regexp_parser-2.11.3": "ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4", + "rubocop-1.81.1": "352a9a6f314a4312f6c305f1f72bc466254d221c95445cd49e1b65d1f9411635", + "rubocop-ast-1.47.1": "592682017855408b046a8190689490763aecea175238232b1b526826349d01ae", + "ruby-progressbar-1.13.0": "80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33", + "unicode-display_width-3.2.0": "0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42", + "unicode-emoji-4.1.0": "4997d2d5df1ed4252f4830a9b6e86f932e2013fbff2182a9ce9ccabda4f325a5", + }, + gemfile = "//:Gemfile", + gemfile_lock = "//:Gemfile.lock", +) +use_repo(ruby, "bundle", "ruby", "ruby_toolchains") + +register_toolchains("@ruby_toolchains//:all") diff --git a/example/WORKSPACE.bazel b/example/WORKSPACE.bazel index e7ef39c2..d7fd57db 100644 --- a/example/WORKSPACE.bazel +++ b/example/WORKSPACE.bazel @@ -334,6 +334,44 @@ register_sarif_parser_toolchains( register = True, ) +http_archive( + name = "rules_ruby", + sha256 = "305d299056a586e24022651eccfe172ea1754121ea79c637e29fc1c42704ae3c", + strip_prefix = "rules_ruby-0.21.1", + url = "https://github.com/bazel-contrib/rules_ruby/releases/download/v0.21.1/rules_ruby-v0.21.1.tar.gz", +) + +load("@rules_ruby//ruby:deps.bzl", "rb_bundle_fetch", "rb_register_toolchains") + +rb_register_toolchains( + version_file = "//:.ruby-version", +) + +rb_bundle_fetch( + name = "bundle", + gem_checksums = { + "ast-2.4.3": "954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383", + "json-2.15.1": "b1c1b2e7c116eb1903e0ce0c374783e6ead8747a0f9eca132d274018ebb80b89", + "json-2.15.1-java": "a6185eebe724a6937f60729e4998276d6b3de3ecc35be34f8e47c1eb40903ecf", + "language_server-protocol-3.17.0.5": "fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc", + "lint_roller-1.1.0": "2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87", + "parallel-1.27.0": "4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130", + "parser-3.3.9.0": "94d6929354b1a6e3e1f89d79d4d302cc8f5aa814431a6c9c7e0623335d7687f2", + "prism-1.5.2": "192741663a55af1ac1b987caa1092deb666e4ff46a30c5064ad5456acd05df1d", + "racc-1.8.1": "4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f", + "racc-1.8.1-java": "54f2e6d1e1b91c154013277d986f52a90e5ececbe91465d29172e49342732b98", + "rainbow-3.1.1": "039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a", + "regexp_parser-2.11.3": "ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4", + "rubocop-1.81.1": "352a9a6f314a4312f6c305f1f72bc466254d221c95445cd49e1b65d1f9411635", + "rubocop-ast-1.47.1": "592682017855408b046a8190689490763aecea175238232b1b526826349d01ae", + "ruby-progressbar-1.13.0": "80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33", + "unicode-display_width-3.2.0": "0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42", + "unicode-emoji-4.1.0": "4997d2d5df1ed4252f4830a9b6e86f932e2013fbff2182a9ce9ccabda4f325a5", + }, + gemfile = "//:Gemfile", + gemfile_lock = "//:Gemfile.lock", +) + #---SNIP--- Below here is re-used in the workspace snippet published on releases load( diff --git a/example/src/BUILD.bazel b/example/src/BUILD.bazel index 01e07a1d..705ab77d 100644 --- a/example/src/BUILD.bazel +++ b/example/src/BUILD.bazel @@ -6,6 +6,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary") load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") load("@rules_proto//proto:defs.bzl", "proto_library") load("@rules_python//python:defs.bzl", "py_library") +load("@rules_ruby//ruby:defs.bzl", "rb_library") load("@rules_shell//shell:sh_library.bzl", "sh_library") load("//tools/lint:linters.bzl", "eslint_test") @@ -40,6 +41,11 @@ filegroup( tags = ["toml"], ) +rb_library( + name = "hello_ruby", + srcs = ["hello.rb"], +) + ts_project( name = "ts_dep", srcs = ["file-dep.ts"], diff --git a/example/src/hello.rb b/example/src/hello.rb new file mode 100644 index 00000000..4e9db63c --- /dev/null +++ b/example/src/hello.rb @@ -0,0 +1,38 @@ +# Demo file showing RuboCop violations + +# Unused variable (will be caught by RuboCop) +unused_variable = "not used" + +# Line too long - this is a demonstration of a line that exceeds the maximum line length configured in .rubocop.yml +def example_method_with_very_long_line + puts "This is an example of a line that is intentionally too long and should be flagged by RuboCop for exceeding the maximum line length" +end + +# Missing frozen string literal comment (if configured) +class ExampleClass + def initialize(name) + @name = name + end + + # Method with trailing whitespace and inconsistent indentation + def greet + puts "Hello, #{@name}!" + end + + # Double negation (style issue) + def active? + !!@active + end +end + +# Prefer single quotes over double quotes for static strings +message = "Hello world" + +# Trailing comma missing in multi-line array +numbers = [ + 1, + 2, + 3 +] + +puts ExampleClass.new("World").greet diff --git a/example/test/BUILD.bazel b/example/test/BUILD.bazel index b3936ad5..8d9edb12 100644 --- a/example/test/BUILD.bazel +++ b/example/test/BUILD.bazel @@ -5,7 +5,17 @@ load("@aspect_rules_ts//ts:defs.bzl", "ts_project") load("@bazel_skylib//rules:write_file.bzl", "write_file") load("@rules_python//python:defs.bzl", "py_library") load("@rules_shell//shell:sh_library.bzl", "sh_library") -load("//tools/lint:linters.bzl", "checkstyle_test", "eslint_test", "flake8_test", "pmd_test", "pylint_test", "ruff_test", "shellcheck_test") +load( + "//tools/lint:linters.bzl", + "checkstyle_test", + "eslint_test", + "flake8_test", + "pmd_test", + "pylint_test", + "rubocop_test", + "ruff_test", + "shellcheck_test", +) write_file( name = "ts_code_generator", @@ -176,3 +186,11 @@ format_test( srcs = [":generated_sh"], tags = ["manual"], ) + +rubocop_test( + name = "rubocop", + srcs = ["//src:hello_ruby"], + # Expected to fail based on current content of the file. + # Normally you'd fix the file instead of tagging this test. + tags = ["manual"], +) diff --git a/example/test/lint_test.bats b/example/test/lint_test.bats index cb7da249..0331f274 100755 --- a/example/test/lint_test.bats +++ b/example/test/lint_test.bats @@ -62,6 +62,13 @@ EOF first-person plural like 'We'. EOF + # RuboCop + echo <<"EOF" | assert_output --partial +C: 1: 1: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment. +W: 4: 1: [Correctable] Lint/UselessAssignment: Useless assignment to variable - unused_variable. +C: 6:101: Layout/LineLength: Line is too long. [115/100] +EOF + # stylelint echo <<"EOF" | assert_output --partial src/hello.css diff --git a/example/tools/java17.bazelrc b/example/tools/java21.bazelrc similarity index 82% rename from example/tools/java17.bazelrc rename to example/tools/java21.bazelrc index 95ffb672..368da940 100644 --- a/example/tools/java17.bazelrc +++ b/example/tools/java21.bazelrc @@ -4,17 +4,17 @@ # 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 +common --java_language_version=21 # 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 +common --tool_java_language_version=21 # 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 +common --java_runtime_version=remotejdk_21 # 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 +common --tool_java_runtime_version=remotejdk_21 diff --git a/example/tools/lint/BUILD.bazel b/example/tools/lint/BUILD.bazel index 77741c68..fe62468e 100644 --- a/example/tools/lint/BUILD.bazel +++ b/example/tools/lint/BUILD.bazel @@ -114,3 +114,8 @@ native_binary( ), out = "clang_tidy", ) + +alias( + name = "rubocop", + actual = "@bundle//bin:rubocop", +) diff --git a/example/tools/lint/linters.bzl b/example/tools/lint/linters.bzl index 8620c1f8..7bb32814 100644 --- a/example/tools/lint/linters.bzl +++ b/example/tools/lint/linters.bzl @@ -9,6 +9,7 @@ load("@aspect_rules_lint//lint:keep_sorted.bzl", "lint_keep_sorted_aspect") load("@aspect_rules_lint//lint:ktlint.bzl", "lint_ktlint_aspect") 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:rubocop.bzl", "lint_rubocop_aspect") load("@aspect_rules_lint//lint:ruff.bzl", "lint_ruff_aspect") load("@aspect_rules_lint//lint:pylint.bzl", "lint_pylint_aspect") load("@aspect_rules_lint//lint:shellcheck.bzl", "lint_shellcheck_aspect") @@ -142,3 +143,10 @@ keep_sorted = lint_keep_sorted_aspect( ) keep_sorted_test = lint_test(aspect = keep_sorted) + +rubocop = lint_rubocop_aspect( + binary = Label("//tools/lint:rubocop"), + configs = [Label("//:.rubocop.yml")], +) + +rubocop_test = lint_test(aspect = rubocop) diff --git a/lint/BUILD.bazel b/lint/BUILD.bazel index bcb82731..307263c8 100644 --- a/lint/BUILD.bazel +++ b/lint/BUILD.bazel @@ -199,6 +199,12 @@ bzl_library( ], ) +bzl_library( + name = "rubocop", + srcs = ["rubocop.bzl"], + deps = ["//lint/private:lint_aspect"], +) + bzl_library( name = "ruff", srcs = ["ruff.bzl"], diff --git a/lint/rubocop.bzl b/lint/rubocop.bzl new file mode 100644 index 00000000..0a87c436 --- /dev/null +++ b/lint/rubocop.bzl @@ -0,0 +1,393 @@ +"""API for declaring a RuboCop lint aspect that visits rb_{binary|library|test} +rules. + +Typical usage: + +## Installing RuboCop + +The recommended approach is to use Bundler with rules_ruby to manage RuboCop +as a gem dependency: + +1. Add RuboCop to your `Gemfile`: +```ruby +gem "rubocop", "~> 1.50" +``` + +2. Run `bundle lock` to generate `Gemfile.lock` + +3. Configure the bundle in your `MODULE.bazel`: +```starlark +ruby = use_extension("@rules_ruby//ruby:extensions.bzl", "ruby") +ruby.toolchain( + name = "ruby", + version = "3.3.0", +) +ruby.bundle_fetch( + name = "bundle", + gemfile = "//:Gemfile", + gemfile_lock = "//:Gemfile.lock", +) +use_repo(ruby, "bundle", "ruby", "ruby_toolchains") +``` + +4. Create an alias to the gem-provided binary in + `tools/lint/BUILD.bazel`: +```starlark +alias( + name = "rubocop", + actual = "@bundle//bin:rubocop", +) +``` + +5. Create the linter aspect, typically in `tools/lint/linters.bzl`: +```starlark +load("@aspect_rules_lint//lint:rubocop.bzl", "lint_rubocop_aspect") + +rubocop = lint_rubocop_aspect( + binary = "//tools/lint:rubocop", + configs = ["//:rubocop.yml"], +) +``` + +This approach ensures: +- Hermetic builds with pinned gem versions +- Consistent RuboCop versions across all developers +- Integration with Bazel's dependency management + +## Configuration + +RuboCop will automatically discover `.rubocop.yml` files according to its +standard configuration hierarchy. +See https://docs.rubocop.org/rubocop/configuration.html for details. + +Note: all config files are passed to the action as inputs. +This means that a change to any config file invalidates the action cache +entries for ALL RuboCop actions. +""" + +load( + "//lint/private:lint_aspect.bzl", + "LintOptionsInfo", + "OPTIONAL_SARIF_PARSER_TOOLCHAIN", + "OUTFILE_FORMAT", + "filter_srcs", + "noop_lint_action", + "output_files", + "parse_to_sarif_action", + "patch_and_output_files", + "should_visit", +) + +_MNEMONIC = "AspectRulesLintRuboCop" + +def _build_rubocop_command(rubocop_path, stdout_path, exit_code_path = None): + """Build shell command for running RuboCop. + + Args: + rubocop_path: path to the RuboCop executable + stdout_path: path where stdout/stderr should be written + exit_code_path: path where exit code should be written. If None, + the command will fail on non-zero exit. + + Returns: + Fully formatted shell command string + """ + cmd_parts = [ + "{rubocop} $@ >{stdout} 2>&1".format( + rubocop = rubocop_path, + stdout = stdout_path, + ), + ] + if exit_code_path: + cmd_parts.append( + "; echo $? >{exit_code}".format(exit_code = exit_code_path), + ) + return "".join(cmd_parts) + +def rubocop_action( + ctx, + executable, + srcs, + config, + stdout, + exit_code = None, + color = False): + """Run RuboCop as an action under Bazel. + + RuboCop will select the configuration file to use for each source file, + as documented here: + https://docs.rubocop.org/rubocop/configuration.html + + Note: all config files are passed to the action. + This means that a change to any config file invalidates the action cache + entries for ALL RuboCop actions. + + However this is needed because RuboCop's logic for selecting the + appropriate config needs to traverse the directory hierarchy. + + Args: + ctx: Bazel Rule or Aspect evaluation context + executable: File object for the RuboCop executable + srcs: list of File objects for Ruby source files to be linted + config: list of File objects for RuboCop config files (.rubocop.yml) + stdout: File object where linter output will be written + exit_code: File object where exit code will be written, or None. + If None, the build will fail when RuboCop exits non-zero. + See https://docs.rubocop.org/rubocop/usage/basic_usage.html + color: boolean, whether to enable color output + """ + inputs = srcs + config + outputs = [stdout] + + # Wire command-line options, see + # `rubocop --help` to see available options + args = ctx.actions.args() + + # Force format to simple for human-readable output + args.add("--format", "simple") + + # Honor exclusions in .rubocop.yml even though we pass explicit list of + # files + args.add("--force-exclusion") + + # Set cache root to /tmp to avoid sandbox permission issues + # RuboCop's server feature needs a writable cache directory + # Note: We can't use --cache false with --cache-root, so we allow caching to /tmp + # Note: We don't pass --no-server because it causes errors with JRuby + args.add("--cache-root", "/tmp") + + # Enable color output if requested + if color: + args.add("--color") + + args.add_all(srcs) + + command = _build_rubocop_command( + executable.path, + stdout.path, + exit_code.path if exit_code else None, + ) + if exit_code: + outputs.append(exit_code) + + ctx.actions.run_shell( + inputs = inputs, + outputs = outputs, + command = command, + arguments = [args], + mnemonic = _MNEMONIC, + progress_message = "Linting %{label} with RuboCop", + tools = [executable], + ) + +def rubocop_fix( + ctx, + executable, + srcs, + config, + patch, + stdout, + exit_code, + color = False): + """Create a Bazel Action that spawns RuboCop with --autocorrect-all. + + Args: + ctx: Bazel Rule or Aspect evaluation context + executable: struct with _rubocop and _patcher fields + srcs: list of File objects for Ruby source files to lint + config: list of File objects for RuboCop config files (.rubocop.yml) + patch: File object where the patch output will be written + stdout: File object where linter output will be written + exit_code: File object where exit code will be written + color: boolean, whether to enable color output + """ + patch_cfg = ctx.actions.declare_file( + "_{}.patch_cfg".format(ctx.label.name), + ) + + # Build args list with color flag if needed + rubocop_args = [ + "--autocorrect-all", + "--force-exclusion", + "--cache", + "false", + ] + if color: + rubocop_args.append("--color") + rubocop_args.extend([s.path for s in srcs]) + + ctx.actions.write( + output = patch_cfg, + content = json.encode({ + "linter": executable._rubocop.path, + "args": rubocop_args, + "files_to_diff": [s.path for s in srcs], + "output": patch.path, + }), + ) + + ctx.actions.run( + inputs = srcs + config + [patch_cfg], + outputs = [patch, exit_code, stdout], + executable = executable._patcher, + arguments = [patch_cfg.path], + env = { + "BAZEL_BINDIR": ".", + "JS_BINARY__EXIT_CODE_OUTPUT_FILE": exit_code.path, + "JS_BINARY__STDOUT_OUTPUT_FILE": stdout.path, + "JS_BINARY__SILENT_ON_SUCCESS": "1", + }, + tools = [executable._rubocop], + mnemonic = _MNEMONIC, + progress_message = "Fixing %{label} with RuboCop", + ) + +# buildifier: disable=function-docstring +def _rubocop_aspect_impl(target, ctx): + if not should_visit( + ctx.rule, + ctx.attr._rule_kinds, + ctx.attr._filegroup_tags, + ): + return [] + + files_to_lint = filter_srcs(ctx.rule) + if ctx.attr._options[LintOptionsInfo].fix: + outputs, info = patch_and_output_files(_MNEMONIC, target, ctx) + else: + outputs, info = output_files(_MNEMONIC, target, ctx) + + if len(files_to_lint) == 0: + noop_lint_action(ctx, outputs) + return [info] + + # RuboCop can produce a patch at the same time as reporting the + # unpatched violations + if hasattr(outputs, "patch"): + rubocop_fix( + ctx, + ctx.executable, + files_to_lint, + ctx.files._config_files, + outputs.patch, + outputs.human.out, + outputs.human.exit_code, + color = ctx.attr._options[LintOptionsInfo].color, + ) + else: + rubocop_action( + ctx, + ctx.executable._rubocop, + files_to_lint, + ctx.files._config_files, + outputs.human.out, + outputs.human.exit_code, + color = ctx.attr._options[LintOptionsInfo].color, + ) + + # Generate machine-readable report in JSON format for SARIF conversion + raw_machine_report = ctx.actions.declare_file( + OUTFILE_FORMAT.format( + label = target.label.name, + mnemonic = _MNEMONIC, + suffix = "raw_machine_report", + ), + ) + + # Create separate action for JSON output + json_args = ctx.actions.args() + + # Use JSON format for machine-readable output (converted to SARIF) + json_args.add("--format", "json") + + # Honor exclusions in .rubocop.yml even though we pass explicit list of + # files + json_args.add("--force-exclusion") + + # Disable caching as Bazel handles caching at the action level + json_args.add("--cache", "false") + json_args.add_all(files_to_lint) + + outputs_list = [raw_machine_report] + command = _build_rubocop_command( + ctx.executable._rubocop.path, + raw_machine_report.path, + outputs.machine.exit_code.path if outputs.machine.exit_code else None, + ) + if outputs.machine.exit_code: + outputs_list.append(outputs.machine.exit_code) + + ctx.actions.run_shell( + inputs = files_to_lint + ctx.files._config_files, + outputs = outputs_list, + command = command, + arguments = [json_args], + mnemonic = _MNEMONIC, + progress_message = """\ +Generating machine-readable report for %{label} with RuboCop\ +""", + tools = [ctx.executable._rubocop], + ) + + parse_to_sarif_action( + ctx, + _MNEMONIC, + raw_machine_report, + outputs.machine.out, + ) + + return [info] + +def lint_rubocop_aspect( + binary, + configs, + rule_kinds = ["rb_binary", "rb_library", "rb_test"], + filegroup_tags = ["ruby", "lint-with-rubocop"]): + """A factory function to create a linter aspect. + + Args: + binary: Label of the RuboCop executable. + Example: "//tools/lint:rubocop" or "@bundle//bin:rubocop" + configs: Label or list of Labels of RuboCop config file(s). + Example: ["//:rubocop.yml"] or "//:rubocop.yml" + rule_kinds: list of rule kinds to visit. + See https://bazel.build/query/language#kind + filegroup_tags: list of filegroup tags. Filegroups with these tags + will be visited by the aspect in addition to Ruby rule kinds. + """ + + # syntax-sugar: allow a single config file in addition to a list + if type(configs) == "string": + configs = [configs] + + return aspect( + implementation = _rubocop_aspect_impl, + attrs = { + "_options": attr.label( + default = "//lint:options", + providers = [LintOptionsInfo], + ), + "_rubocop": attr.label( + default = binary, + allow_files = True, + executable = True, + cfg = "exec", + ), + "_patcher": attr.label( + default = "@aspect_rules_lint//lint/private:patcher", + executable = True, + cfg = "exec", + ), + "_config_files": attr.label_list( + default = configs, + allow_files = True, + ), + "_filegroup_tags": attr.string_list( + default = filegroup_tags, + ), + "_rule_kinds": attr.string_list( + default = rule_kinds, + ), + }, + toolchains = [OPTIONAL_SARIF_PARSER_TOOLCHAIN], + )