From 9e1e493ffea715236687d80f183835b9d602e1eb Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:02:17 +0200 Subject: [PATCH] [Fix #13061] Add new `Style/RedundantInterpolationUnfreeze` cop In Ruby >= 3.0, interpolated strings are always unfrozen: https://bugs.ruby-lang.org/issues/17104 On Ruby 2.7 they followed the magic comment literal which made it necessary to sometimes unfreeze them manually. This could also apply on Ruby 2.7 by looking at the magic comment but it doesn't seem worth the effort. RuboCop itself has 5 offenses: ``` Offenses: lib/rubocop/cop/lint/unused_method_argument.rb:99:21: C: [Correctable] Style/RedundantInterpolationUnfreeze: Don't unfreeze interpolated strings as they are already unfrozen message = +"Unused method argument - `#{variable.name}`." ^ lib/rubocop/cops_documentation_generator.rb:133:15: C: [Correctable] Style/RedundantInterpolationUnfreeze: Don't unfreeze interpolated strings as they are already unfrozen content = +"==== #{title}\n" ^ lib/rubocop/cops_documentation_generator.rb:259:15: C: [Correctable] Style/RedundantInterpolationUnfreeze: Don't unfreeze interpolated strings as they are already unfrozen content = +<<~HEADER ^ lib/rubocop/cops_documentation_generator.rb:307:15: C: [Correctable] Style/RedundantInterpolationUnfreeze: Don't unfreeze interpolated strings as they are already unfrozen content = +"=== Department xref:#{filename}[#{type_title}]\n\n" ^ spec/rubocop/cop/layout/end_of_line_spec.rb:21:16: C: [Correctable] Style/RedundantInterpolationUnfreeze: Don't unfreeze interpolated strings as they are already unfrozen input = (+<<~RUBY).force_encoding(encoding) ^ 1546 files inspected, 5 offenses detected, 5 offenses autocorrectable ``` --- ..._style_redundant_interpolation_unfreeze.md | 1 + config/default.yml | 5 + lib/rubocop.rb | 1 + .../cop/mixin/frozen_string_literal.rb | 5 +- .../style/redundant_interpolation_unfreeze.rb | 46 +++++++ .../redundant_interpolation_unfreeze_spec.rb | 118 ++++++++++++++++++ 6 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 changelog/new_style_redundant_interpolation_unfreeze.md create mode 100644 lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb create mode 100644 spec/rubocop/cop/style/redundant_interpolation_unfreeze_spec.rb diff --git a/changelog/new_style_redundant_interpolation_unfreeze.md b/changelog/new_style_redundant_interpolation_unfreeze.md new file mode 100644 index 000000000000..19624fb8501e --- /dev/null +++ b/changelog/new_style_redundant_interpolation_unfreeze.md @@ -0,0 +1 @@ +* [#13061](https://github.com/rubocop/rubocop/issues/13061): Add new `Style/RedundantInterpolationUnfreeze` cop to check for `dup` and `@+` on interpolated strings in Ruby >= 3.0. ([@earlopain][]) diff --git a/config/default.yml b/config/default.yml index bc52d408ecf7..9233144ef20d 100644 --- a/config/default.yml +++ b/config/default.yml @@ -5057,6 +5057,11 @@ Style/RedundantInterpolation: VersionAdded: '0.76' VersionChanged: '1.30' +Style/RedundantInterpolationUnfreeze: + Description: 'Checks for redundant unfreezing of interpolated strings.' + Enabled: pending + VersionAdded: '<>' + Style/RedundantLineContinuation: Description: 'Check for redundant line continuation.' Enabled: pending diff --git a/lib/rubocop.rb b/lib/rubocop.rb index f8f7fbcbcd76..9140b6eaf7bd 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -583,6 +583,7 @@ require_relative 'rubocop/cop/style/redundant_filter_chain' require_relative 'rubocop/cop/style/redundant_heredoc_delimiter_quotes' require_relative 'rubocop/cop/style/redundant_initialize' +require_relative 'rubocop/cop/style/redundant_interpolation_unfreeze' require_relative 'rubocop/cop/style/redundant_line_continuation' require_relative 'rubocop/cop/style/redundant_regexp_argument' require_relative 'rubocop/cop/style/redundant_regexp_constructor' diff --git a/lib/rubocop/cop/mixin/frozen_string_literal.rb b/lib/rubocop/cop/mixin/frozen_string_literal.rb index d32e196af8bc..315880708bd6 100644 --- a/lib/rubocop/cop/mixin/frozen_string_literal.rb +++ b/lib/rubocop/cop/mixin/frozen_string_literal.rb @@ -20,7 +20,7 @@ def frozen_string_literal_comment_exists? def frozen_string_literal?(node) frozen_string = if target_ruby_version >= 3.0 - uninterpolated_string?(node) || frozen_heredoc?(node) + uninterpolated_string?(node) || uninterpolated_heredoc?(node) else FROZEN_STRING_LITERAL_TYPES_RUBY27.include?(node.type) end @@ -32,11 +32,12 @@ def uninterpolated_string?(node) node.str_type? || (node.dstr_type? && node.each_descendant(:begin).none?) end - def frozen_heredoc?(node) + def uninterpolated_heredoc?(node) return false unless node.dstr_type? && node.heredoc? node.children.all?(&:str_type?) end + alias frozen_heredoc? uninterpolated_heredoc? def frozen_string_literals_enabled? ruby_version = processed_source.ruby_version diff --git a/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb b/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb new file mode 100644 index 000000000000..0765c1a20044 --- /dev/null +++ b/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Style + # Before Ruby 3.0, interpolated strings followed the frozen string literal + # magic comment which sometimes made it necessary to explicitly unfreeze them. + # Ruby 3.0 changed interpolated strings to always be unfrozen which makes + # unfreezing them redundant. + # + # @example + # # bad + # +"#{foo} bar" + # + # # bad + # "#{foo} bar".dup + # + # # good + # "#{foo} bar" + # + class RedundantInterpolationUnfreeze < Base + include FrozenStringLiteral + extend AutoCorrector + extend TargetRubyVersion + + MSG = "Don't unfreeze interpolated strings as they are already unfrozen." + + RESTRICT_ON_SEND = %i[+@ dup].freeze + + minimum_target_ruby_version 3.0 + + def on_send(node) + return if node.arguments? + return unless (receiver = node.receiver) + return unless receiver.dstr_type? + return if uninterpolated_string?(receiver) || uninterpolated_heredoc?(receiver) + + add_offense(node.loc.selector) do |corrector| + corrector.remove(node.loc.selector) + corrector.remove(node.loc.dot) unless node.unary_operation? + end + end + end + end + end +end diff --git a/spec/rubocop/cop/style/redundant_interpolation_unfreeze_spec.rb b/spec/rubocop/cop/style/redundant_interpolation_unfreeze_spec.rb new file mode 100644 index 000000000000..918d886a7149 --- /dev/null +++ b/spec/rubocop/cop/style/redundant_interpolation_unfreeze_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Style::RedundantInterpolationUnfreeze, :config do + context 'target_ruby_version >= 3.0', :ruby30 do + it 'registers an offense for `@+`' do + expect_offense(<<~'RUBY') + +"#{foo} bar" + ^ Don't unfreeze interpolated strings as they are already unfrozen. + RUBY + + expect_correction(<<~'RUBY') + "#{foo} bar" + RUBY + end + + it 'registers an offense for `@+` as a normal method call' do + expect_offense(<<~'RUBY') + "#{foo} bar".+@ + ^^ Don't unfreeze interpolated strings as they are already unfrozen. + RUBY + + expect_correction(<<~'RUBY') + "#{foo} bar" + RUBY + end + + it 'registers an offense for `dup`' do + expect_offense(<<~'RUBY') + "#{foo} bar".dup + ^^^ Don't unfreeze interpolated strings as they are already unfrozen. + RUBY + + expect_correction(<<~'RUBY') + "#{foo} bar" + RUBY + end + + it 'registers an offense for interpolated heredoc with `@+`' do + expect_offense(<<~'RUBY') + foo(+<<~MSG) + ^ Don't unfreeze interpolated strings as they are already unfrozen. + foo #{bar} + baz + MSG + RUBY + + expect_correction(<<~'RUBY') + foo(<<~MSG) + foo #{bar} + baz + MSG + RUBY + end + + it 'registers an offense for interpolated heredoc with `dup`' do + expect_offense(<<~'RUBY') + foo(<<~MSG.dup) + ^^^ Don't unfreeze interpolated strings as they are already unfrozen. + foo #{bar} + baz + MSG + RUBY + + expect_correction(<<~'RUBY') + foo(<<~MSG) + foo #{bar} + baz + MSG + RUBY + end + + it 'registers no offense for uninterpolated heredoc' do + expect_no_offenses(<<~'RUBY') + foo(+<<~'MSG') + foo #{bar} + baz + MSG + RUBY + end + + it 'registers no offense for plain string literals' do + expect_no_offenses(<<~RUBY) + "foo".dup + RUBY + end + + it 'registers no offense for other types' do + expect_no_offenses(<<~RUBY) + local.dup + RUBY + end + + it 'registers no offense when the method has arguments' do + expect_no_offenses(<<~'RUBY') + "#{foo} bar".dup(baz) + RUBY + end + + it 'registers no offense for multiline string literals' do + expect_no_offenses(<<~RUBY) + +'foo' \ + 'bar' + RUBY + end + + it 'registers no offense when there is no receiver' do + expect_no_offenses(<<~RUBY) + dup + RUBY + end + end + + context 'target_ruby_version < 3.0', :ruby27, unsupported_on: :prism do + it 'accepts unfreezing an interpolated string' do + expect_no_offenses('+"#{foo} bar"') + end + end +end