Skip to content

Enable Style/FrozenStringLiteralComment - #86

Merged
dduugg merged 1 commit into
mainfrom
enable-frozen-string-literal-comment
Sep 26, 2026
Merged

dduugg merged 1 commit into
mainfrom
enable-frozen-string-literal-comment

Conversation

@dduugg

@dduugg dduugg commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

Why

# frozen_string_literal: true makes plain string literals immutable. Code that accidentally mutates a shared literal then raises FrozenError right away instead of silently corrupting the value. It also lets Ruby reuse one object per literal instead of allocating a new String each time the line runs. Ruby 3.4+ already warns about chilled-literal mutation, and a future Ruby will make frozen literals the default, so turning this on now keeps the gem ahead of that change.

Config change

  • .rubocop.yml: removed the Style/FrozenStringLiteralComment: Enabled: false block. The cop now runs with its default EnforcedStyle: always.
  • .rubocop_todo.yml has no entry for this cop and is unchanged. No new Exclude was needed.

Files that got the pragma

The pragma was added to 17 files. These are every file the cop flagged.

  • 11 in lib/:
    • basic_reference_offense.rb
    • check/default_formatter.rb
    • check/offenses_formatter.rb
    • packwerk_wrapper.rb
    • private.rb
    • private/git.rb
    • private/ownership_information.rb
    • private/todo_yml_changes.rb
    • update/default_formatter.rb
    • update/offenses_formatter.rb
    • violation_diff.rb
  • Rakefile and sorbet/tapioca/require.rb (hand-maintained, not generated)
  • 3 spec files and spec/support/danger_plugin.rb

In the lib/ files the pragma goes on the line after # typed: strict. That matches the 5 lib files that already had it, and both positions are valid magic-comment locations. Files with no sigil get a blank line after the pragma (Layout/EmptyLineAfterMagicComment).

Nothing was excluded. A few files were left alone:

  • bin/* binstubs: already excluded by AllCops: Exclude, and they already have the pragma.
  • Gemfile and the gemspec: already had the pragma.
  • Generated RBIs under sorbet/rbi/**: rubocop never inspects them, because AllCops: Include has no *.rbi pattern.

Runtime fixes

None were needed. Every string literal in the newly pragma'd files was traced to its uses, including into packwerk, danger, constant_resolver and code_ownership. None of them is mutated.

Every mutating call in lib/ has a non-String receiver:

  • Array#<<: basic_reference_offense.rb:66 and todo_yml_changes.rb:74
  • Hash#[]=: todo_yml_changes.rb:97
  • ENV#[]=: packwerk_wrapper.rb:29
  • Tempfile#<<: todo_yml_changes.rb:162, with an interpolated argument

The gem requires Ruby >= 3.3, and CI runs 3.3, 3.4 and 4.0. On those versions interpolated strings and interpolated heredocs stay unfrozen, so every formatter message handed to Danger is unfrozen, same as before.

A few methods now return frozen literals:

  • OffensesAggregatorFormatter#show_offenses, #show_stale_violations and #show_strict_mode_violations return ''. Packwerk only uses these in select(&:present?).join("\n") + "\n".
  • #identifier is only used as a hash key.
  • Private::OwnershipInformation#ownership_copy returns '- This pack is unowned.'. It is private API, and its callers only interpolate the value.

Uncovered code paths. Specs don't reach packwerk_wrapper.rb:54-70 or check/default_formatter.rb:54. They also stub Packwerk::Cli#execute_command, RailsLoadPaths.for, Open3.capture3 and git.diff. The dynamic checks below exercised those paths for real.

Verification

  • Specs: 67 examples, 0 failures on origin/main before the change. After it: 67 examples, 0 failures on Ruby 4.0.7 and 3.3.11. The runs used RUBYOPT=-W:deprecated and RUBYOPT=--enable-frozen-string-literal, with 0 FrozenErrors and 0 "literal string will be frozen" warnings.

  • Rubocop: 26 files inspected, no offenses.

  • srb tc: no errors.

  • Static sweep: read every non-RBI Ruby file line by line, and cross-checked with a Prism scan of every non-interpolated literal and every mutating call. Library code that receives our literals was read as well:

    • packwerk: cli, check_command, update_todo_command, offenses_formatter, rails_load_paths
    • danger 9.6.0: messaging, Markdown, Violation
    • constant_resolver 0.3.0
  • Pragma in effect: compiled every file with RubyVM::InstructionSequence on 4.0.7 and 3.3.11. There were no putstring or putchilledstring instructions, so the pragma is active in every file, including where it sits after the Sorbet sigil.

  • Dynamic checks: end-to-end runs against real Rails/packwerk/code_ownership git fixtures, on 4.0.7 and 3.3.11. They used:

    • a real Danger::Dangerfile
    • a real GitRepo diff
    • a real packwerk check through OffensesAggregatorFormatter
    • a real Private.constant_resolver
    • a real git apply --reverse through the Tempfile write
  • Dynamic coverage: the runs covered:

    • modified, added and deleted package_todo.yml files, plus renames
    • owned, unowned and root packs
    • root_path both nil and a subdirectory
    • both grouping strategies
    • fail_build and on_failure
    • both default formatters, with and without repo_url_builder and a custom help message

    No FrozenError, and every package_todo.yml was restored afterwards.

  • Fresh Eyes: a local pre-push review (fresh-eyes local --base origin/main) was run on this exact diff (commit 802b439, 18 files). It found 0 blockers, 0 major, 0 minor and 0 info findings.

Remove the `Style/FrozenStringLiteralComment: Enabled: false` entry from
.rubocop.yml so the cop runs with its default EnforcedStyle (always).
.rubocop_todo.yml has no entry for this cop.

Add `# frozen_string_literal: true` to the 17 files the cop flagged:
Rakefile, sorbet/tapioca/require.rb, 11 files under lib/, and 4 spec
files. In the `# typed:` files the pragma goes on the line after the
sigil, which is how the files that already had it are laid out. The
files without a sigil also get a blank line after the pragma
(Layout/EmptyLineAfterMagicComment).

No mutation fixes were needed. Every mutating call in lib/, spec/, bin/,
the gemspec, the Gemfile and the Rakefile acts on an Array, a Hash, a
Tempfile, or a string that isn't a literal. The frozen literals in the
newly covered files are only compared, interpolated, used as ENV values
or hash keys, or passed to non-mutating methods (delete_prefix, delete,
gsub, fetch). OffensesAggregatorFormatter returns a frozen '' to
Packwerk, which only calls present?/join on it. The generated RBIs under
sorbet/rbi/ are not inspected by rubocop, so they are unchanged.
@dduugg
dduugg requested a review from a team as a code owner September 26, 2026 16:48
@github-project-automation github-project-automation Bot moved this to Triage in Modularity Sep 26, 2026
@dduugg
dduugg merged commit 4d95224 into main Sep 26, 2026
9 checks passed
@dduugg
dduugg deleted the enable-frozen-string-literal-comment branch September 26, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant