Skip to content

Update rubocop to 1.89.0 and rubocop-sorbet to 0.14.0 - #23452

Open
dduugg wants to merge 2 commits into
Homebrew:mainfrom
dduugg:rubocop-1.89.0
Open

Update rubocop to 1.89.0 and rubocop-sorbet to 0.14.0#23452
dduugg wants to merge 2 commits into
Homebrew:mainfrom
dduugg:rubocop-1.89.0

Conversation

@dduugg

@dduugg dduugg commented Aug 5, 2026

Copy link
Copy Markdown
Member

Updates rubocop from 1.88.2 to 1.89.0 and rubocop-sorbet from 0.13.2 to 0.14.0, along with json (2.21.1 to 2.21.2) which rubocop depends on.

rubocop 1.89.0

Two suppressions can go away thanks to bug fixes in this release:

One suppression is added. rubocop/rubocop#15493 makes Style/IfUnlessModifier respect Layout/LineLength's exemptions instead of its Max when deciding whether the modifier form fits. Several of our exemption patterns are unescaped regexes ("#{version." matches #{version}, #{version_text} and #{version_info[...]}), so the cop demanded modifier form for statements that then ran to 179 characters. There is no config toggle for the new behaviour, so the cop is disabled. See the discussion below for the four affected sites.

This is reported upstream as rubocop/rubocop#15531, with a minimal reproduction, asking that the modifier cops keep checking the joined line against Max or that the new behaviour be made configurable. Style/WhileUntilModifier and Style/GuardClause are affected by the same change, but neither produces offenses here today, so both stay enabled.

The rest are new offenses in this release:

rubocop-sorbet 0.14.0

This widens Sorbet/SetterReturnType to cover setters taking splat and keyword arguments, so several depends_on and ENV setters switch to .void.

Under HOMEBREW_SORBET_RUNTIME a .void method returns the sentinel T::Private::Types::Void::VOID rather than the value the body produced. That breaks the two callers that consumed a setter's return value, neither of which Sorbet catches statically.

Cask::DSL::DependsOn#load stored the setter result into the delegated hash backing the whole stanza:

__getobj__[key] = case key
when :macos, :maximum_macos
  send(:"#{key}=", *value, set_in_block:)
else
  send(:"#{key}=", *value)
end

so every depends_on value became VOID, producing Required (1): T::Private::Types::Void::VOID (cask) and undefined method 'keys' for module T::Private::Types::Void::VOID. It now calls the setter for its side effect and reads the value back through the matching reader, which returns the same object the setter previously returned.

Superenv#cc= consumed the return value of super:

sig { params(val: T.any(String, Pathname)).void }
def cc=(val)
  self["HOMEBREW_CC"] = super
end

which raises once SharedEnvExtension#cc= becomes .void:

extend/ENV/super.rb:160:in '[]=': no implicit conversion of Module into String (TypeError)

It now calls super and assigns from val.

A .void return can only leak through send, super or an implicit return, since assignment syntax always evaluates to the right-hand side. Those two were the only consumers. ENV.clang and friends now return VOID because the COMPILERS.each blocks end on send(:cxx=, ...), but nothing reads those returns, and there are no extend/os overrides of cc=, cxx= or []=.

Dropping extend/ENV/shared.rbi

[]= is a setter, so the widened Sorbet/SetterReturnType applies to the shim too and its return type had to become .void:

sig {
  type_parameters(:U).params(
    key:   String,
    value: T.all(T.type_parameter(:U), T.nilable(T.any(String, Pathname, PATH))),
  ).returns(T.type_parameter(:U))
}
def []=(key, value); end

The whole point of the generic was to return the assigned value's type, which a void return makes pointless. That left a hand-written shim whose only remaining job was widening the parameter to accept Pathname and PATH, so I found it best to drop it and be explicit at the affected call sites instead.

Without the shim, []= resolves to Sorbet's own ENVClass#[]=, which takes T.nilable(String). The ENV assignments now convert explicitly: &.to_s where the value is nilable (T.nilable(PATH)), .to_s where it is not (PATH, Pathname, or a || "" that already removed nil).

This is not a behaviour change. Both Pathname and PATH define to_str, so ENV["X"] = some_path was already converting implicitly, and the explicit calls just make it visible to the typechecker.

Also drops the redundant T.let on GitHub::API::ERRORS.


  • Have you followed our Contributing guidelines?
  • Have you checked for other open Pull Requests for the same change?
  • Have you explained what your changes do? Performance claims (e.g. "this is faster") must include Hyperfine benchmarks.
  • Have you explained why you'd like these changes included, not just what they do?
  • For bug fixes, have you given step-by-step brew commands to reproduce the bug?
  • Have you written new tests (excluding integration tests)? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) locally?

  • I did not use AI/LLM to create this PR, or I disclosed the tool/model below and reviewed its output; I did not attribute commits to AI and will answer maintainer questions and review comments myself without AI/LLM.

Claude Code (Opus 5) ran brew vendor-gems --update, brew style --fix and brew typecheck --update, then drafted this description. I directed the handling of each cop change, including the decision to disable Style/IfUnlessModifier rather than accept its autocorrections. Several autocorrections were rejected after checking them against the vendored Ruby rather than trusting brew typecheck, which passes clean on all of them: Pathname#each_filename.respond_to?(:intersect?) returns false, and both VOID bugs above were reproduced under HOMEBREW_SORBET_RUNTIME. The DependsOn#load fix was checked red/green against brew tests --only=cask/info. The obsolete tap-new.rb workaround was confirmed by reproducing the original pattern under ruby -w. brew lgtm --online and the full brew tests suite pass locally.


@dduugg
dduugg marked this pull request as ready for review August 5, 2026 19:34
Copilot AI lite review requested due to automatic review settings August 5, 2026 19:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates Homebrew鈥檚 vendored RuboCop to 1.89.0 (and json to 2.21.2) and applies the resulting style/autocorrect changes across the codebase, including removing now-unnecessary RuboCop suppressions and adjusting a call site to avoid an Enumerator#intersect? runtime error.

Changes:

  • Bump vendored rubocop to 1.89.0 and json to 2.21.2 (including lockfile + bundler setup paths).
  • Re-enable/lean on Style/ArrayIntersect by updating affected call sites to use intersect? safely.
  • Remove obsolete Layout/HashAlignment suppression comments and adjust new offenses from the RuboCop update; disable Style/IfUnlessModifier due to new behaviour.

Reviewed changes

Copilot reviewed 8 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Library/Homebrew/vulns/identify.rb Adjust indentation for multiline chained call.
Library/Homebrew/vendor/bundle/bundler/setup.rb Update vendored load paths for json 2.21.2 and rubocop 1.89.0.
Library/Homebrew/test/install_steps_spec.rb Switch from manual intersection+empty check to Array#intersect?.
Library/Homebrew/tap_auditor.rb Remove redundant parentheses around hash literal passed to <<.
Library/Homebrew/sorbet/rbi/gems/rubocop@1.89.0.rbi Update RuboCop RBI to match 1.89.0 API surface.
Library/Homebrew/Gemfile.lock Bump json and rubocop versions and checksums.
Library/Homebrew/formula.rb Remove now-dead Layout/HashAlignment suppression on delegate calls.
Library/Homebrew/formula_auditor.rb Remove redundant parentheses around hash literals passed to <<.
Library/Homebrew/dev-cmd/tap-new.rb Remove now-unnecessary duplicate assignment workaround for Ruby warnings.
Library/Homebrew/cask/artifact/abstract_uninstall.rb Avoid calling intersect? on the Enumerator from each_filename by converting to an array.
Library/.rubocop.yml Remove Style/ArrayIntersect disable; disable Style/IfUnlessModifier.

馃挕 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Library/.rubocop.yml
Comment on lines +443 to +445
# Avoid a postfixed conditional being obscured by a long preceding statement.
Style/IfUnlessModifier:
Enabled: false
@MikeMcQuaid

Copy link
Copy Markdown
Member

One suppression is added. rubocop/rubocop#15493 makes Style/IfUnlessModifier respect Layout/LineLength's exemptions instead of its Max when deciding whether the modifier form fits. Several of our exemption patterns are unescaped regexes ("#{version." matches #{version}, #{version_text} and #{version_info[...]}), so the cop demanded modifier form for statements that then ran to 179 characters. There is no config toggle for the new behaviour, so the cop is disabled.

Can you say more about this one? Some examples would be good.

@dduugg

dduugg commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

@MikeMcQuaid sure thing!
before:

unless quiet
  opoo "Not upgrading #{cask.token}, the installed version is not below the minimum version #{version}"
end

after:

opoo "Not upgrading #{cask.token}, the installed version is not below the minimum version #{version}" unless quiet

Happy to keep the cop if you prefer the correction (or want to use it selectively), but I found the corrections to excessively obscure a postfixed conditional.

@dduugg dduugg changed the title Update rubocop to 1.89.0 Update rubocop to 1.89.0 and rubocop-sorbet to 0.14.0 Aug 5, 2026
@MikeMcQuaid

Copy link
Copy Markdown
Member

@dduugg yeh I agree not ideal on long lines. Worth filing an issue on this though I think about respecting the existing LineLength or at least allowing customisability?

meefs pushed a commit to meefs/homebrew-cask that referenced this pull request Aug 5, 2026
The `zap` stanza ended with a `\` followed by a blank line, so the
continuation joined nothing. RuboCop 1.89.0 flags this as
`Style/RedundantLineContinuation`, which fails `brew style homebrew/cask`
on Homebrew/brew#23452.

Claude-Session: https://claude.ai/code/session_01AdSQcU5MmY4VWiFdfrPeu2
Also updates rubocop-sorbet (0.13.2 -> 0.14.0) and json (2.21.1 ->
2.21.2), which rubocop depends on.

Two suppressions can go away thanks to bug fixes in this release:

- rubocop/rubocop#15438 and rubocop/rubocop#15442 stop
  `Style/ArrayIntersect` firing when the `include?` receiver in a block
  is not an array literal, so the cop is re-enabled. It still cannot
  guarantee the *outer* receiver is an `Array`, and in
  `cask/artifact/abstract_uninstall.rb` that receiver is the `Enumerator`
  from `Pathname#each_filename`, which has no `intersect?`. Convert with
  `to_a` there rather than taking the autocorrect verbatim.
- rubocop/rubocop#15452 fixes the `Layout/HashAlignment` false positive on
  multi-line hash keys, so the two `delegate` disable comments in
  `formula.rb` are dead and removed.

One suppression is added: rubocop/rubocop#15493 makes
`Style/IfUnlessModifier` respect `Layout/LineLength`'s exemptions rather
than its `Max`. Several of our exemption patterns are unescaped regexes
(`"#{version."` matches `#{version}`, `#{version_text}` and
`#{version_info[...]}`), so the cop demanded modifier form for statements
that then ran to 179 characters. Disable the cop. Reported upstream as
rubocop/rubocop#15531.

rubocop-sorbet 0.14.0 widens `Sorbet/SetterReturnType` to cover setters
taking splat and keyword arguments, so several `depends_on` and ENV
setters switch to `.void`. Under `HOMEBREW_SORBET_RUNTIME` a `.void`
method returns the sentinel `T::Private::Types::Void::VOID`, which breaks
the two callers that consumed a setter's return value. Sorbet does not
catch either statically:

- `DependsOn#load` stored the result of `send(:"#{key}=", ...)` into the
  delegated hash, so the whole `depends_on` stanza became `VOID`. Read the
  value back through the matching reader instead.
- `Superenv#cc=` assigned `super` to `HOMEBREW_CC`, raising `TypeError:
  no implicit conversion of Module into String`. Assign from `val`.

Drop `extend/ENV/shared.rbi`, which existed to widen `[]=` to accept
`Pathname` and `PATH`. Without it `[]=` resolves to Sorbet's own
`ENVClass#[]=`, which takes `T.nilable(String)`, so the ENV assignments
convert explicitly with `to_s`/`&.to_s`. Both `Pathname` and `PATH`
define `to_str`, so Ruby was already converting implicitly and behaviour
is unchanged.

The remaining changes are new offenses in this release:

- `Lint/UselessAssignment` (rubocop/rubocop#12269) catches the
  `root_url = root_url =` workaround in `dev-cmd/tap-new.rb`. The Ruby
  `assigned but unused variable` warning it silenced no longer fires on
  our vendored Ruby, so drop it.
- `Style/RedundantParentheses` (rubocop/rubocop#15472) and
  `Layout/MultilineMethodCallIndentation` autocorrections.

The redundant `T.let` on `GitHub::API::ERRORS` is also dropped.

Claude-Session: https://claude.ai/code/session_01AdSQcU5MmY4VWiFdfrPeu2
@dduugg

dduugg commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

I think I鈥檓 coming around to the idea of disabling the new sorbet cop that requires setters to be void. It removes some nice ergonomics without much upside.

Comment thread Library/.rubocop.yml
Enabled: true

# Removes some ergonomic use of setter return values, without much upside.
Sorbet/SetterReturnType:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to be annoying: honestly I think I prefer the enabled version above? I think it's a bit more explicit and intuitive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants