Skip to content

Enable Style/FrozenStringLiteralComment - #64

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

With # frozen_string_literal: true, string literals are frozen. Code that accidentally mutates a literal (or a constant built from one) then raises FrozenError right away instead of silently corrupting shared state. It also cuts allocations: each literal is allocated once and reused, not rebuilt every time the line runs. Ruby 3.4+ already warns about mutating these "chilled" literals, so enabling the pragma now puts this gem ahead of that default change.

Config change

.rubocop.yml: deleted the block that disabled the cop:

# Disabling for now until it's clearer why we want this
Style/FrozenStringLiteralComment:
  Enabled: false

The cop now runs with its default EnforcedStyle: always. Nothing else in the config changed.

Files that got the pragma

11 files: Gemfile, Rakefile, parse_packwerk.gemspec, lib/parse_packwerk.rb, lib/parse_packwerk/{configuration,constants,extensions,package,package_todo,violation}.rb, and spec/support/have_matching_package.rb. lib/parse_packwerk/package_set.rb, spec/spec_helper.rb and spec/parse_packwerk_spec.rb already had it.

  • In files with a Sorbet # typed: sigil, the pragma goes on the line after the sigil, which is where package_set.rb already has it. srb tc passes, and disassembling the files confirms Ruby honors the pragma in that position.
  • lib/parse_packwerk/constants.rb: Style/RedundantFreeze flagged the explicit .freeze on the 12 string constants (ROOT_PACKAGE_NAME, PACKAGE_YML_NAME, and so on), so I removed it. The pragma now freezes them, so their frozenness and interning are unchanged.
  • Exclusions: none. RuboCop only inspects Gemfile, Rakefile, the gemspec, lib/**/*.rb and spec/**/*.rb. The generated sorbet/rbi/**/*.rbi files are never inspected or executed, so they need no pragma and no Exclude entry.

Runtime fixes

None were needed. No path in the gem passes a frozen literal to a mutating call. The only << in lib/ is Array#<< on a local accumulator (lib/parse_packwerk/package_todo.rb:29). Every other literal is only used as a hash key, an include? or == argument, a File.open mode, a File.join/Pathname#join/fnmatch argument, or an argument to the non-bang gsub in write_package_yml!.

One visible behavior change (not a bug)

DEFAULT_PUBLIC_PATH (lib/parse_packwerk/constants.rb:26, 'app/public') is now frozen. It is the default: for Package#public_path (lib/parse_packwerk/package.rb:12) and the fallback in Package.from (lib/parse_packwerk/package.rb:32). As a result:

  • Package.from already returned the shared constant object on main. Mutating it there (for example pkg.public_path << '/x') silently changed the default for every package. It now raises FrozenError.
  • Package.new without public_path: sorbet-runtime used to deep-clone the unfrozen default for each instance. The default is frozen now, so it shares the constant, and mutating it raises FrozenError instead of changing a private copy.

public_path is a const on the struct and was never meant to be mutated. Nothing in this gem mutates it. I also checked the other rubyatscale gems that use ParsePackwerk (packs, pack_stats, visualize_packs, query_packwerk, danger-packwerk, rubocop-packs): they only interpolate, join, compare, or pass the value back into Package.new/with.

For the same reason, the elements of ParsePackwerk.key_sort_order and the default elements of ParsePackwerk.yml.package_paths ('**/' and '.') are now frozen too. The arrays themselves stay mutable. No consumer mutates these elements.

Verification

  • Specs: bundle exec rspec gives 55 examples, 0 failures both before (main) and after, on Ruby 4.0.5. On this branch the suite also passes on Ruby 3.3 and 3.4, the other CI versions. It passes under RUBYOPT="-W:deprecated --debug=frozen-string-literal" with no chilled-string warnings, and under --enable-frozen-string-literal (freeze everything globally). bundle exec rake (default task: spec) also passes.
  • RuboCop: bundle exec rubocop inspected 14 files with no offenses.
  • Sorbet: bundle exec srb tc reports no errors.
  • Packaging: gem build parse_packwerk.gemspec, rake build and rake -T all work with the pragma in the gemspec, Gemfile and Rakefile.
  • Static sweep: I read every Ruby file line by line and traced each string literal to where it ends up, looking for bang methods, <</concat/[]=, force_encoding, string buffers and StringIO, mutable default arguments, mutated constants, literals returned to callers that mutate them, and literals passed to libraries that mutate their argument. I found none. I also checked the library code these values flow into: sorbet-runtime ApplyDefault, Psych YAML.dump, and the RubyGems spec setters.
  • Dynamic checks: a script built a temp packwerk fixture and exercised the public API: all, find, package_from_path with a String or Pathname, violations, public_directory, yml, write_package_yml! with preserve_key_order true and false, bust_cache!, MissingConfiguration, and Configuration.fetch with a missing file, a comment-only file and a scalar exclude. The script also covered the paths the specs don't reach. I ran it against this branch and against main. The outputs match except for the frozenness changes described above.
  • Fresh Eyes: a local pre-push review reported 0 blockers, 0 major, 0 minor and 0 info findings.

No version bump.

Remove the .rubocop.yml entry that disabled Style/FrozenStringLiteralComment
so the cop runs with its default EnforcedStyle (always).

Add "# frozen_string_literal: true" to the 11 files the cop flagged: Gemfile,
Rakefile, parse_packwerk.gemspec, every file under lib/ that lacked it, and
spec/support/have_matching_package.rb. In files with a Sorbet "# typed:"
sigil, the pragma goes on the line after the sigil, matching the existing
package_set.rb.

Drop the now-redundant .freeze calls on the string constants in
lib/parse_packwerk/constants.rb (Style/RedundantFreeze). The literals are
still frozen, now through the pragma.

No mutation fixes were needed. The suite shows no FrozenError and no
"literal string will be frozen" warnings under -W:deprecated, and a static
sweep of lib/ and the build files found no string-mutating call on a literal.
One visible effect: Package#public_path defaults to 'app/public', which is
now a frozen String. Package.from already returned the shared
DEFAULT_PUBLIC_PATH constant, so mutating it would have corrupted every
package; now it raises instead.
@dduugg
dduugg requested a review from a team as a code owner September 26, 2026 16:41
@github-project-automation github-project-automation Bot moved this to Triage in Modularity Sep 26, 2026
@dduugg
dduugg merged commit fad8f3e into main Sep 26, 2026
9 checks passed
@dduugg
dduugg deleted the enable-frozen-string-literal-comment branch September 26, 2026 18:38
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