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