Replace standardrb with rubocop-gusto and enable frozen string literals - #76
Merged
Merged
Conversation
Lint with the shared rubocop-gusto configuration used across the other rubyatscale gems instead of Standard. - Gemfile: drop standard, add rubocop-gusto (require: false). The lockfile change is limited to removing standard's gems and adding rubocop-gusto's. sorbet-static (pulled in through rubocop-sorbet) has no pure-Ruby build, so the lockfile now lists concrete platforms instead of `ruby`. - .rubocop.yml: inherit rubocop-gusto's default and sidekiq configs, target Ruby 3.3 (the gemspec minimum), keep RuboCop's default excludes, and disable the Sorbet department because this gem does not use Sorbet. - Remove .standard.yml. - Replace the StandardRB workflow with a RuboCop workflow that runs on push and pull_request with read-only permissions.
Safe autocorrections (rubocop -a): shorthand hash values, hash brace
spacing, trailing commas in multiline literals, percent-literal
delimiters, the `->` lambda literal, block delimiters, symbol-style RSpec
metadata, the unused Sidekiq middleware `queue` argument renamed to
`_queue`, and removal of disable directives for cops that are not loaded
(Rails/TimeZone) or not enabled (Metrics/AbcSize), along with the matching
orphaned `rubocop:enable`.
Unsafe autocorrections, applied one cop at a time and reviewed:
- Style/FrozenStringLiteralComment: add the magic comment to the 13 files
that lacked it, including exe/singed, plus the blank line after it that
Layout/EmptyLineAfterMagicComment requires.
- Gusto/RedundantSpecHelperRequire: drop `require "spec_helper"` from the
Sidekiq spec; .rspec already passes `--require spec_helper`.
- Style/HashEachMethods: `list.each { |_addr, frame| }` becomes
`list.each_value { |frame| }` in Singed::Report#filter!.
StackProf::Report#frames always returns a Hash.
Library: - Singed::ControllerExt: use ActiveSupport::Concern instead of a hand-written `self.included` hook (Gusto/NoMetaprogramming). Including the module still extends the host with ClassMethods, and Singed::ControllerExt::ClassMethods is unchanged. A new spec covers this. - Singed::Sidekiq::ServerMiddleware: move TRUTHY_STRINGS above `private`, which never applied to it (Lint/UselessConstantScoping). The constant stays public, as it was before. - Singed: keep `extend self` with an inline Style/ModuleFunction disable. `extend self` makes every Singed method a public instance method of the module as well as a module method. Moving them into `class << self` would remove those instance methods for anyone who includes or extends Singed. Specs: - Replace allow_/expect_any_instance_of (RSpec/AnyInstance). The Rack middleware spec stubs Singed::Speedscope.open. The Sidekiq spec hands Sidekiq and ActiveJob the middleware and job instances it asserts against. Breaking the middleware on purpose (always profiling, never profiling, or dropping the job) still fails the suite. - Rename middleware_spec.rb and sidekiq_spec.rb to match the classes they describe (RSpec/SpecFilePathFormat), and drop the "extension" suffix from the Kernel spec's describe. - Reword contexts to start with when/with (RSpec/ContextWording). The "default" Kernel#flamegraph context now omits `open:` so it actually tests the default, rather than repeating the `open: true` context (RSpec/RepeatedExampleGroupBody). - Call the private Speedscope.os_open_command with __send__ (Style/Send). Adding the frozen_string_literal comment everywhere caused no FrozenError. A review of every mutating call in lib/, exe/, bin/ and the Rakefile found no string literal that is mutated after it is created.
The "adds the flamegraph class method" example asserted respond_to(:flamegraph), which every object satisfies because Kernel#flamegraph is public. It now checks that the method is owned by ControllerExt::ClassMethods, so it fails if the Concern stops extending ClassMethods. The comment on `extend self` claimed every method is also an instance method; `self.output_directory` is singleton-only, so say plain `def` methods instead.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The other rubyatscale gems (pack_stats, danger-migrations, and others) lint with rubocop-gusto. singed was the odd one out on standardrb. Switching gives these gems one shared lint config.
It also turns on
Style/FrozenStringLiteralComment(EnforcedStyle: always_true). Standard disables that cop, and 13 of singed's Ruby files lacked the pragma. All 26 linted files now have it.Tooling changes
standardand addedrubocop-gusto(require: false).standard,standard-custom, andstandard-performance.rubocop-gusto11.9.0 and its transitive dependencies (rubocop-rspec,rubocop-rake,rubocop-sorbet,sorbet-static/sorbet-runtime,spoom,rbi,rbs,code_teams,smart_todo, and a few small ones).PLATFORMSchanged fromrubytoaarch64-linux,arm64-darwin,x86_64-darwin, andx86_64-linux.sorbet-static, pulled in viarubocop-sorbet, has no pure-ruby build, so Bundler dropsruby. The new list covers GitHub's ubuntu runners and macOS dev machines.BUNDLED WITHis unchanged..standard.yml: deleted..rubocop.yml: rewritten to follow therubocop-gusto inittemplate.inherit_gempulls inrubocop-gusto'sconfig/default.ymlandconfig/sidekiq.yml. singed ships Sidekiq middleware, which is whatinitdetects.pluginslistsrubocop-gusto,rubocop-rspec,rubocop-performance, andrubocop-rake.inherit_mode: mergecoversExclude/Include, so RuboCop's default excludes survive alongsidevendor/**/*.TargetRubyVersionstays at 3.3..github/workflows/standardrb.yamlis replaced by.github/workflows/rubocop.yml.standardrb/standard-ruby-action@v1onpushonly, withchecks: writeandcontents: write.bundle exec rubocopon[push, pull_request]withcontents: read, on Ruby 3.4.actions/checkoutandruby/setup-rubypinned to SHAs, the same pinsrubocop-gusto's own lint job uses, pluspersist-credentials: false.actionlintandzizmor --offline.build.yml,dependabot.yml, the README, and AGENTS.md needed no changes. AGENTS.md already documentsbundle exec rubocop.Offenses and how they were fixed
The first run found 105 offenses in 21 of 25 files. Adding the pragma then produced 12
Layout/EmptyLineAfterMagicCommentoffenses, which were fixed with safe-a. The final run inspects 26 files with 0 offenses, on Ruby 3.3, 3.4, and 4.0..newto return a known instance, with noany_instance-a(shorthandlabel:)-A, reviewed (below)-awith .../when running on ...)-a__send__, becauseos_open_commandis private-a:Rails/TimeZoneisn't loaded andMetrics/AbcSizeisn't enabled; also removed the orphanedrubocop:enablegit mvof two spec files, anddescribe Kernelopen:-aControllerExtusesActiveSupport::Concern-A, reviewed (below)-A, reviewed (below)TRUTHY_STRINGSaboveprivate(constants were never private, so it stays public)-a: Layout/FirstMethodArgumentLineBreak, Layout/SpaceInLambdaLiteral, Lint/UnusedMethodArgument (queuebecame_queue), RSpec/MetadataStyle, RSpec/ScatteredLet, Style/BlockDelimiters, Style/CommentAnnotation, Style/Lambda, Style/StringLiteralsInInterpolationNotable manual changes:
lib/singed/controller_ext.rb:3-7:self.included(base); base.extend(ClassMethods)becameextend ActiveSupport::Concern, which requiresactive_support/concern.ControllerExtis only loaded from the Railtie, where ActiveSupport is always present.ActionController::Base.spec/singed/controller_ext_spec.rbcovers the class method and thearound_actionwiring. A mutation test showed both examples fail if the Concern stops extendingClassMethods.spec/singed/middleware_spec.rbwas renamed tospec/singed/rack_middleware_spec.rb.spec/singed/sidekiq_spec.rbwas renamed tospec/singed/sidekiq/server_middleware_spec.rb.described_class.newandjob_class.newin place ofany_instance. A mutation test (always profile, never profile, drop the job) showed it still catches regressions: 5-6 failures per mutation.Singed::Speedscope.openin place ofFlamegraph#openon any instance.Cops configured or disabled
Sorbetdepartment is disabled in.rubocop.yml.sorbet/config, and no typecheck step.Sorbet/ValidSigilandSorbet/StrictSigilwould add# typed:sigils that nothing reads.# typed: strictwould claim typing the gem doesn't have.rubocop-gusto's own.rubocop.ymldisables the department for the same reason.config/sorbet.ymlis not inherited.initonly detected Sorbet becauserubocop-sorbetput it in the lockfile.# rubocop:disable Style/ModuleFunctiononextend selfatlib/singed.rb:10, with a comment explaining why.extend selfmakes each plain-defmethod onSingedboth a module method and a public instance method, and the gem has shipped that way since its first release.class << selfordef self., would remove those instance methods for anyone who includes or extendsSinged. That is a public API change for a published gem, which is out of scope here.No other cops are configured or disabled, and there is no
.rubocop_todo.yml.NewCopsinheritsrubocop-gusto's setting.Unsafe autocorrects and frozen-string fixes
No frozen-string fixes were needed. No literal in
lib/,exe/,bin/, theRakefile, orspec/is ever mutated, and the verification below backs this up.Unsafe autocorrects, each applied one cop at a time and reviewed:
Style/FrozenStringLiteralCommentadded the pragma to 13 files, all at line 1 exceptexe/singed:2, where it follows the shebang:exe/singed:2lib/singed/backtrace_cleaner_ext.rb:1,lib/singed/cli.rb:1,lib/singed/controller_ext.rb:1,lib/singed/flamegraph.rb:1,lib/singed/kernel_ext.rb:1,lib/singed/rack_middleware.rb:1,lib/singed/railtie.rb:1,lib/singed/report.rb:1,lib/singed/rspec.rb:1spec/singed/kernel_ext_spec.rb:1,spec/spec_helper.rb:1,spec/support/sidekiq.rb:1putobject.Gusto/RedundantSpecHelperRequireremovedrequire "spec_helper"atspec/singed/sidekiq/server_middleware_spec.rb:3..rspecalready passes--require spec_helper, and the file passes run on its own.Style/HashEachMethodschangedlist.each do |_addr, frame|tolist.each_value do |frame|atlib/singed/report.rb:32.StackProf::Report#framesalways returns a Hash (built withinject({})in stackprof 0.2.13 through 0.2.28, which covers the gemspec's>= 0.2.13range), and the key was already unused.Three public strings are now frozen where they weren't on main: the
ArgumentErrormessages fromFlamegraph.new,Singed::CLI#opts.banner, and the elements ofRackMiddleware::TRUTHY_STRINGS. Nothing in singed or its dependencies mutates them. Only outside code that appends to one of them in place would notice.Verification
bundle exec rspecgives 42 examples, 0 failures on Ruby 3.3.11, 3.4.11, and 4.0.5. The 40-example baseline all still passes; the 2 new examples are the ControllerExt spec.RUBYOPT=--enable-frozen-string-literal, in 10 random orders, and with each file run alone.bundle exec rubocopreports 26 files, no offenses on all three Rubies.rubocop -Lincludesexe/singed,bin/*, theRakefile, theGemfile, and the gemspec.exe/,lib/,bin/, and theRakefile; all are frozen. A grep of every mutating call (<<, bang methods,force_encoding,[]=, read buffers, and so on) found no mutated literal. Literals handed to libraries were traced through each one:bannerand switch strings are never mutated.shellescapedups beforegsub!.Kernel#system, Pathname, and Rails initializer names.join.-W:deprecated, all paths below gave zero "literal string will be frozen" warnings. The warning mechanism was confirmed to fire.exe/singed -o ... -r 50 -- ruby script.rbrun, with PATH shims forsudo,rbspy,npx, andopen, covered the password check, the rbspy record,chown, the JSON frame filter and rewrite, and the speedscope open. It was run with and without global frozen literals.Kernel#flamegraphwithopen:true and false, andSinged.start/stop/saveReport#filter!throughActiveSupport::BacktraceCleanerSpeedscope.openbranches (npx and bundled)RackMiddlewarewithX-Singedoutput_directoryand the backtrace cleaner and installed the middleware.flamegraph :showand anX-Singedrequest each wrote a flamegraph.FrozenError.fresh-eyes local --base origin/main) over the final diff: 26 files, 21 applicable checks. It found 0 blockers, 0 major, and 0 minor. Its only note (info) was that the title it inferred from the last commit subject undersold the scope, which this PR title addresses.Pre-existing issues seen during verification, not introduced or fixed here, and the same on main:
exe/singed --bogusraises an uncaughtOptionParser::InvalidOption, becauseopts.ordersits outside therescue.-rpasses a bare--rateto rbspy.X-Singedon an action that also usesflamegraph :action) raisesNoMethodErrorinside stackprof.exe/singedraisesNameErroronPathname, becausecli.rbdoesn't requirepathname.