From beefc7bcd069f5ccee4c3324893cdcd399f60f56 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 15 Oct 2025 17:18:06 -0600 Subject: [PATCH 1/5] feat: add Standard Ruby linter support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for Standard Ruby (standardrb) as a Ruby linter alongside RuboCop. This implementation follows the same pattern as the RuboCop integration with full support for linting, auto-fixing, and SARIF report generation. Key changes: - Add lint/standardrb.bzl with lint_standardrb_aspect for Ruby linting - Update example configuration with standardrb gem dependency (v1.51.1) - Add .standard.yml configuration file for the example project - Integrate standardrb into the example tooling and test suite - Add test expectations for Standard Ruby linting violations - Update README.md to document Standard Ruby support - Pin gem versions in MODULE.bazel and WORKSPACE.bazel for reproducible builds Standard Ruby is a zero-configuration Ruby linter and formatter based on RuboCop. It provides stricter defaults and enforces a consistent style guide. 🤖 Generated with Claude Code Co-Authored-By: Claude --- README.md | 3 +- example/.standard.yml | 18 ++ example/BUILD.bazel | 1 + example/Gemfile | 1 + example/Gemfile.lock | 21 +- example/MODULE.bazel | 6 +- example/WORKSPACE.bazel | 6 +- example/test/BUILD.bazel | 10 + example/test/lint_test.bats | 9 + example/tools/lint/BUILD.bazel | 5 + example/tools/lint/linters.bzl | 10 +- lint/BUILD.bazel | 6 + lint/standardrb.bzl | 381 +++++++++++++++++++++++++++++++++ 13 files changed, 471 insertions(+), 6 deletions(-) create mode 100644 example/.standard.yml create mode 100644 lint/standardrb.bzl diff --git a/README.md b/README.md index fe9d7e30..d5754471 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Linters which are not language-specific: | Markdown | [Prettier] | [Vale] | | Protocol Buffer | [buf] | [buf lint] | | Python | [ruff] | [flake8], [pylint], [ruff] | -| Ruby | | [RuboCop] | +| Ruby | | [RuboCop], [Standard] | | Rust | [rustfmt] | | | SQL | [prettier-plugin-sql] | | | Scala | [scalafmt] | | @@ -85,6 +85,7 @@ Linters which are not language-specific: [jsonnetfmt]: https://github.com/google/go-jsonnet [scalafmt]: https://scalameta.org/scalafmt [rubocop]: https://docs.rubocop.org/ +[standard]: https://github.com/standardrb/standard [ruff]: https://docs.astral.sh/ruff/ [pylint]: https://pylint.readthedocs.io/en/stable/ [shellcheck]: https://www.shellcheck.net/ diff --git a/example/.standard.yml b/example/.standard.yml new file mode 100644 index 00000000..f04775ca --- /dev/null +++ b/example/.standard.yml @@ -0,0 +1,18 @@ +# Standard Ruby configuration for rules_lint example +# +# Standard Ruby is a zero-configuration Ruby style guide, linter, and formatter +# based on RuboCop. See https://github.com/standardrb/standard +# +# This file allows minimal customization of Standard Ruby's opinionated defaults. + +# Configure target Ruby version +ruby_version: 3.0 + +# Ignore certain paths (glob patterns) +ignore: + - 'vendor/**/*' + - 'tmp/**/*' + +# You can configure specific cops if needed, though Standard discourages this +# fix: false # Uncomment to prevent auto-fixing +# parallel: true # Uncomment to enable parallel processing diff --git a/example/BUILD.bazel b/example/BUILD.bazel index 30360a42..83c55446 100644 --- a/example/BUILD.bazel +++ b/example/BUILD.bazel @@ -21,6 +21,7 @@ exports_files( "checkstyle-suppressions.xml", ".ruff.toml", ".rubocop.yml", + ".standard.yml", ".shellcheckrc", ".scalafmt.conf", ".swcrc", diff --git a/example/Gemfile b/example/Gemfile index d4790124..758e34d6 100644 --- a/example/Gemfile +++ b/example/Gemfile @@ -3,3 +3,4 @@ source 'https://rubygems.org' gem 'rubocop', '~> 1.50' +gem 'standard', '>= 1.35.1' diff --git a/example/Gemfile.lock b/example/Gemfile.lock index c48e1460..2ff14fb7 100644 --- a/example/Gemfile.lock +++ b/example/Gemfile.lock @@ -15,7 +15,7 @@ GEM racc (1.8.1-java) rainbow (3.1.1) regexp_parser (2.11.3) - rubocop (1.81.1) + rubocop (1.80.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -23,13 +23,29 @@ GEM 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) + rubocop-ast (>= 1.46.0, < 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) + rubocop-performance (1.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) ruby-progressbar (1.13.0) + standard (1.51.1) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.80.2) + standard-custom (~> 1.0.0) + standard-performance (~> 1.8) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.8.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.25.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.1.0) @@ -42,6 +58,7 @@ PLATFORMS DEPENDENCIES rubocop (~> 1.50) + standard (>= 1.35.1) BUNDLED WITH 2.6.9 diff --git a/example/MODULE.bazel b/example/MODULE.bazel index 9e3ccfa3..66bbcc72 100644 --- a/example/MODULE.bazel +++ b/example/MODULE.bazel @@ -150,9 +150,13 @@ ruby.bundle_fetch( "racc-1.8.1-java": "54f2e6d1e1b91c154013277d986f52a90e5ececbe91465d29172e49342732b98", "rainbow-3.1.1": "039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a", "regexp_parser-2.11.3": "ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4", - "rubocop-1.81.1": "352a9a6f314a4312f6c305f1f72bc466254d221c95445cd49e1b65d1f9411635", + "rubocop-1.80.2": "6485f30fefcf5c199db3b91e5e253b1ef43f7e564784e2315255809a3dd9abf4", "rubocop-ast-1.47.1": "592682017855408b046a8190689490763aecea175238232b1b526826349d01ae", + "rubocop-performance-1.25.0": "6f7d03568a770054117a78d0a8e191cefeffb703b382871ca7743831b1a52ec1", "ruby-progressbar-1.13.0": "80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33", + "standard-1.51.1": "6d0d98a1fac26d660393f37b3d9c864632bb934b17abfa23811996b20f87faf2", + "standard-custom-1.0.2": "424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b", + "standard-performance-1.8.0": "ed17b7d0e061b2a19a91dd434bef629439e2f32310f22f26acb451addc92b788", "unicode-display_width-3.2.0": "0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42", "unicode-emoji-4.1.0": "4997d2d5df1ed4252f4830a9b6e86f932e2013fbff2182a9ce9ccabda4f325a5", }, diff --git a/example/WORKSPACE.bazel b/example/WORKSPACE.bazel index d7fd57db..03435982 100644 --- a/example/WORKSPACE.bazel +++ b/example/WORKSPACE.bazel @@ -362,9 +362,13 @@ rb_bundle_fetch( "racc-1.8.1-java": "54f2e6d1e1b91c154013277d986f52a90e5ececbe91465d29172e49342732b98", "rainbow-3.1.1": "039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a", "regexp_parser-2.11.3": "ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4", - "rubocop-1.81.1": "352a9a6f314a4312f6c305f1f72bc466254d221c95445cd49e1b65d1f9411635", + "rubocop-1.80.2": "6485f30fefcf5c199db3b91e5e253b1ef43f7e564784e2315255809a3dd9abf4", "rubocop-ast-1.47.1": "592682017855408b046a8190689490763aecea175238232b1b526826349d01ae", + "rubocop-performance-1.25.0": "6f7d03568a770054117a78d0a8e191cefeffb703b382871ca7743831b1a52ec1", "ruby-progressbar-1.13.0": "80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33", + "standard-1.51.1": "6d0d98a1fac26d660393f37b3d9c864632bb934b17abfa23811996b20f87faf2", + "standard-custom-1.0.2": "424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b", + "standard-performance-1.8.0": "ed17b7d0e061b2a19a91dd434bef629439e2f32310f22f26acb451addc92b788", "unicode-display_width-3.2.0": "0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42", "unicode-emoji-4.1.0": "4997d2d5df1ed4252f4830a9b6e86f932e2013fbff2182a9ce9ccabda4f325a5", }, diff --git a/example/test/BUILD.bazel b/example/test/BUILD.bazel index 8d9edb12..280afa32 100644 --- a/example/test/BUILD.bazel +++ b/example/test/BUILD.bazel @@ -3,6 +3,7 @@ load("@aspect_rules_lint//format:defs.bzl", "format_test") load("@aspect_rules_ts//ts:defs.bzl", "ts_project") load("@bazel_skylib//rules:write_file.bzl", "write_file") +load("@rules_java//java:java_library.bzl", "java_library") load("@rules_python//python:defs.bzl", "py_library") load("@rules_shell//shell:sh_library.bzl", "sh_library") load( @@ -15,6 +16,7 @@ load( "rubocop_test", "ruff_test", "shellcheck_test", + "standardrb_test", ) write_file( @@ -194,3 +196,11 @@ rubocop_test( # Normally you'd fix the file instead of tagging this test. tags = ["manual"], ) + +standardrb_test( + name = "standardrb", + 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 0331f274..15778e12 100755 --- a/example/test/lint_test.bats +++ b/example/test/lint_test.bats @@ -67,6 +67,15 @@ EOF 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 + + # Standard Ruby + echo <<"EOF" | assert_output --partial +== src/hello.rb == +W: 4: 1: [Correctable] Lint/UselessAssignment: Useless assignment to variable - unused_variable. +C: 19: 3: [Correctable] Layout/IndentationWidth: Use 2 (not 4) spaces for indentation. +W: 29: 1: [Correctable] Lint/UselessAssignment: Useless assignment to variable - message. +W: 32: 1: [Correctable] Lint/UselessAssignment: Useless assignment to variable - numbers. EOF # stylelint diff --git a/example/tools/lint/BUILD.bazel b/example/tools/lint/BUILD.bazel index fe62468e..2d78c480 100644 --- a/example/tools/lint/BUILD.bazel +++ b/example/tools/lint/BUILD.bazel @@ -119,3 +119,8 @@ alias( name = "rubocop", actual = "@bundle//bin:rubocop", ) + +alias( + name = "standardrb", + actual = "@bundle//bin:standardrb", +) diff --git a/example/tools/lint/linters.bzl b/example/tools/lint/linters.bzl index 7bb32814..ae24ed6a 100644 --- a/example/tools/lint/linters.bzl +++ b/example/tools/lint/linters.bzl @@ -9,11 +9,12 @@ 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:pylint.bzl", "lint_pylint_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") load("@aspect_rules_lint//lint:spotbugs.bzl", "lint_spotbugs_aspect") +load("@aspect_rules_lint//lint:standardrb.bzl", "lint_standardrb_aspect") load("@aspect_rules_lint//lint:stylelint.bzl", "lint_stylelint_aspect") load("@aspect_rules_lint//lint:vale.bzl", "lint_vale_aspect") load("@aspect_rules_lint//lint:yamllint.bzl", "lint_yamllint_aspect") @@ -150,3 +151,10 @@ rubocop = lint_rubocop_aspect( ) rubocop_test = lint_test(aspect = rubocop) + +standardrb = lint_standardrb_aspect( + binary = Label("//tools/lint:standardrb"), + configs = [Label("//:.standard.yml")], +) + +standardrb_test = lint_test(aspect = standardrb) diff --git a/lint/BUILD.bazel b/lint/BUILD.bazel index 307263c8..71ed80d4 100644 --- a/lint/BUILD.bazel +++ b/lint/BUILD.bazel @@ -205,6 +205,12 @@ bzl_library( deps = ["//lint/private:lint_aspect"], ) +bzl_library( + name = "standardrb", + srcs = ["standardrb.bzl"], + deps = ["//lint/private:lint_aspect"], +) + bzl_library( name = "ruff", srcs = ["ruff.bzl"], diff --git a/lint/standardrb.bzl b/lint/standardrb.bzl new file mode 100644 index 00000000..78c06a9c --- /dev/null +++ b/lint/standardrb.bzl @@ -0,0 +1,381 @@ +"""API for declaring a Standard Ruby lint aspect that visits rb_{binary|library|test} +rules. + +Typical usage: + +## Installing Standard Ruby + +The recommended approach is to use Bundler with rules_ruby to manage Standard Ruby +as a gem dependency: + +1. Add Standard Ruby to your `Gemfile`: +```ruby +gem "standard", "~> 1.0" +``` + +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 = "standardrb", + actual = "@bundle//bin:standardrb", +) +``` + +5. Create the linter aspect, typically in `tools/lint/linters.bzl`: +```starlark +load("@aspect_rules_lint//lint:standardrb.bzl", "lint_standardrb_aspect") + +standardrb = lint_standardrb_aspect( + binary = "//tools/lint:standardrb", + configs = ["//:standard.yml"], +) +``` + +This approach ensures: +- Hermetic builds with pinned gem versions +- Consistent Standard Ruby versions across all developers +- Integration with Bazel's dependency management + +## Configuration + +Standard Ruby will automatically discover `.standard.yml` files according to its +standard configuration hierarchy. +See https://github.com/standardrb/standard 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 Standard Ruby 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 = "AspectRulesLintStandardRB" + +def _build_standardrb_command(standardrb_path, stdout_path, exit_code_path = None): + """Build shell command for running Standard Ruby. + + Args: + standardrb_path: path to the Standard Ruby 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 = [ + "{standardrb} $@ >{stdout} 2>&1".format( + standardrb = standardrb_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 standardrb_action( + ctx, + executable, + srcs, + config, + stdout, + exit_code = None, + color = False): + """Run Standard Ruby as an action under Bazel. + + Standard Ruby will select the configuration file to use for each source file, + as documented here: + https://github.com/standardrb/standard + + 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 Standard Ruby actions. + + However this is needed because Standard Ruby'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 Standard Ruby executable + srcs: list of File objects for Ruby source files to be linted + config: list of File objects for Standard Ruby config files (.standard.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 Standard Ruby exits non-zero. + See https://github.com/standardrb/standard + color: boolean, whether to enable color output + """ + inputs = srcs + config + outputs = [stdout] + + # Wire command-line options, see + # `standardrb --help` to see available options + args = ctx.actions.args() + + # Force format to simple for human-readable output + args.add("--format", "simple") + + # Disable caching as Bazel handles caching at the action level + args.add("--cache", "false") + + # Enable color output if requested + if color: + args.add("--color") + + args.add_all(srcs) + + command = _build_standardrb_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 Standard Ruby", + tools = [executable], + ) + +def standardrb_fix( + ctx, + executable, + srcs, + config, + patch, + stdout, + exit_code, + color = False): + """Create a Bazel Action that spawns Standard Ruby with --fix. + + Args: + ctx: Bazel Rule or Aspect evaluation context + executable: struct with _standardrb and _patcher fields + srcs: list of File objects for Ruby source files to lint + config: list of File objects for Standard Ruby config files (.standard.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 + standardrb_args = [ + "--fix", + "--cache", + "false", + ] + if color: + standardrb_args.append("--color") + standardrb_args.extend([s.path for s in srcs]) + + ctx.actions.write( + output = patch_cfg, + content = json.encode({ + "linter": executable._standardrb.path, + "args": standardrb_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._standardrb], + mnemonic = _MNEMONIC, + progress_message = "Fixing %{label} with Standard Ruby", + ) + +# buildifier: disable=function-docstring +def _standardrb_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] + + # Standard Ruby can produce a patch at the same time as reporting the + # unpatched violations + if hasattr(outputs, "patch"): + standardrb_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: + standardrb_action( + ctx, + ctx.executable._standardrb, + 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") + + # 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_standardrb_command( + ctx.executable._standardrb.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 Standard Ruby\ +""", + tools = [ctx.executable._standardrb], + ) + + parse_to_sarif_action( + ctx, + _MNEMONIC, + raw_machine_report, + outputs.machine.out, + ) + + return [info] + +def lint_standardrb_aspect( + binary, + configs, + rule_kinds = ["rb_binary", "rb_library", "rb_test"], + filegroup_tags = ["ruby", "lint-with-standardrb"]): + """A factory function to create a linter aspect. + + Args: + binary: Label of the Standard Ruby executable. + Example: "//tools/lint:standardrb" or "@bundle//bin:standardrb" + configs: Label or list of Labels of Standard Ruby config file(s). + Example: ["//:standard.yml"] or "//:standard.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 = _standardrb_aspect_impl, + attrs = { + "_options": attr.label( + default = "//lint:options", + providers = [LintOptionsInfo], + ), + "_standardrb": 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], + ) From 53ae1d3e63739123026149df55dda0e5443a8c5b Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 15 Oct 2025 17:26:51 -0600 Subject: [PATCH 2/5] fix: add missing --force-exclusion flag to standardrb linter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address code review findings: - Add --force-exclusion flag in standardrb_action() to honor exclusions from .standard.yml even when explicit files are passed - Add --force-exclusion flag in JSON output action for consistency - Update Gemfile to use ~> version constraint for standard gem to match RuboCop pattern and prevent breaking changes This ensures Standard Ruby respects exclusion patterns defined in configuration files, matching the behavior of the RuboCop implementation. 🤖 Generated with Claude Code Co-Authored-By: Claude --- example/Gemfile | 2 +- example/Gemfile.lock | 2 +- lint/standardrb.bzl | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/example/Gemfile b/example/Gemfile index 758e34d6..ab7f81d5 100644 --- a/example/Gemfile +++ b/example/Gemfile @@ -3,4 +3,4 @@ source 'https://rubygems.org' gem 'rubocop', '~> 1.50' -gem 'standard', '>= 1.35.1' +gem 'standard', '~> 1.35' diff --git a/example/Gemfile.lock b/example/Gemfile.lock index 2ff14fb7..a99681d3 100644 --- a/example/Gemfile.lock +++ b/example/Gemfile.lock @@ -58,7 +58,7 @@ PLATFORMS DEPENDENCIES rubocop (~> 1.50) - standard (>= 1.35.1) + standard (~> 1.35) BUNDLED WITH 2.6.9 diff --git a/lint/standardrb.bzl b/lint/standardrb.bzl index 78c06a9c..23298b3d 100644 --- a/lint/standardrb.bzl +++ b/lint/standardrb.bzl @@ -146,6 +146,10 @@ def standardrb_action( # Force format to simple for human-readable output args.add("--format", "simple") + # Honor exclusions in .standard.yml even though we pass explicit list of + # files + args.add("--force-exclusion") + # Disable caching as Bazel handles caching at the action level args.add("--cache", "false") @@ -292,6 +296,10 @@ def _standardrb_aspect_impl(target, ctx): # Use JSON format for machine-readable output (converted to SARIF) json_args.add("--format", "json") + # Honor exclusions in .standard.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) From 0d9deb5448a5e24946874206be4b05308beb2d88 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Tue, 4 Nov 2025 13:08:04 -0700 Subject: [PATCH 3/5] fix: ensure standardrb uses /tmp for cache and does not fix in tests --- lint/standardrb.bzl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lint/standardrb.bzl b/lint/standardrb.bzl index 23298b3d..1bb9ed94 100644 --- a/lint/standardrb.bzl +++ b/lint/standardrb.bzl @@ -150,8 +150,15 @@ def standardrb_action( # files args.add("--force-exclusion") - # Disable caching as Bazel handles caching at the action level - args.add("--cache", "false") + # Set cache root to /tmp to avoid sandbox permission issues + # StandardRB uses RuboCop internally, which 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") + + # Disable auto-fix to prevent writing to input files in the sandbox + # Tests should only report violations, not modify source files + args.add("--no-fix") # Enable color output if requested if color: From d4499c189e2303efd7d12687b47527dd6bc8ac98 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 5 Nov 2025 12:26:35 -0700 Subject: [PATCH 4/5] style: wrap long lines in standardrb.bzl to 80 chars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensure all lines in lint/standardrb.bzl adhere to the 80 character limit for Starlark files per project conventions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lint/standardrb.bzl | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lint/standardrb.bzl b/lint/standardrb.bzl index 1bb9ed94..a8187823 100644 --- a/lint/standardrb.bzl +++ b/lint/standardrb.bzl @@ -1,12 +1,13 @@ -"""API for declaring a Standard Ruby lint aspect that visits rb_{binary|library|test} -rules. +"""API for declaring a Standard Ruby lint aspect. + +Visits rb_{binary|library|test} rules. Typical usage: ## Installing Standard Ruby -The recommended approach is to use Bundler with rules_ruby to manage Standard Ruby -as a gem dependency: +The recommended approach is to use Bundler with rules_ruby to manage +Standard Ruby as a gem dependency: 1. Add Standard Ruby to your `Gemfile`: ```ruby @@ -80,7 +81,10 @@ load( _MNEMONIC = "AspectRulesLintStandardRB" -def _build_standardrb_command(standardrb_path, stdout_path, exit_code_path = None): +def _build_standardrb_command( + standardrb_path, + stdout_path, + exit_code_path = None): """Build shell command for running Standard Ruby. Args: @@ -114,8 +118,8 @@ def standardrb_action( color = False): """Run Standard Ruby as an action under Bazel. - Standard Ruby will select the configuration file to use for each source file, - as documented here: + Standard Ruby will select the configuration file to use for each + source file, as documented here: https://github.com/standardrb/standard Note: all config files are passed to the action. @@ -129,7 +133,8 @@ def standardrb_action( ctx: Bazel Rule or Aspect evaluation context executable: File object for the Standard Ruby executable srcs: list of File objects for Ruby source files to be linted - config: list of File objects for Standard Ruby config files (.standard.yml) + config: list of File objects for Standard Ruby config files + (.standard.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 Standard Ruby exits non-zero. @@ -151,8 +156,9 @@ def standardrb_action( args.add("--force-exclusion") # Set cache root to /tmp to avoid sandbox permission issues - # StandardRB uses RuboCop internally, which needs a writable cache directory - # Note: We can't use --cache false with --cache-root, so we allow caching to /tmp + # StandardRB uses RuboCop internally, which 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") @@ -199,7 +205,8 @@ def standardrb_fix( ctx: Bazel Rule or Aspect evaluation context executable: struct with _standardrb and _patcher fields srcs: list of File objects for Ruby source files to lint - config: list of File objects for Standard Ruby config files (.standard.yml) + config: list of File objects for Standard Ruby config files + (.standard.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 From daf3e85a2a5c7b737ca7719c7192c4ad8672bd47 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 5 Nov 2025 12:30:33 -0700 Subject: [PATCH 5/5] fix: remove unintended change --- example/test/BUILD.bazel | 1 - 1 file changed, 1 deletion(-) diff --git a/example/test/BUILD.bazel b/example/test/BUILD.bazel index 280afa32..2b52d0bd 100644 --- a/example/test/BUILD.bazel +++ b/example/test/BUILD.bazel @@ -3,7 +3,6 @@ load("@aspect_rules_lint//format:defs.bzl", "format_test") load("@aspect_rules_ts//ts:defs.bzl", "ts_project") load("@bazel_skylib//rules:write_file.bzl", "write_file") -load("@rules_java//java:java_library.bzl", "java_library") load("@rules_python//python:defs.bzl", "py_library") load("@rules_shell//shell:sh_library.bzl", "sh_library") load(