diff --git a/changelog/change_recognize_shareable_constant_value_magic.md b/changelog/change_recognize_shareable_constant_value_magic.md new file mode 100644 index 00000000000..ea94ae81a26 --- /dev/null +++ b/changelog/change_recognize_shareable_constant_value_magic.md @@ -0,0 +1 @@ +* [#9328](https://github.com/rubocop-hq/rubocop/issues/9328): Recognize shareable_constant_value magic comment. ([@caalberts][]) diff --git a/lib/rubocop.rb b/lib/rubocop.rb index c1891b089a1..712c09d6912 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -113,6 +113,7 @@ require_relative 'rubocop/cop/mixin/rational_literal' require_relative 'rubocop/cop/mixin/rescue_node' require_relative 'rubocop/cop/mixin/safe_assignment' +require_relative 'rubocop/cop/mixin/shareable_constant_value' require_relative 'rubocop/cop/mixin/space_after_punctuation' require_relative 'rubocop/cop/mixin/space_before_punctuation' require_relative 'rubocop/cop/mixin/surrounding_space' diff --git a/lib/rubocop/cop/mixin/shareable_constant_value.rb b/lib/rubocop/cop/mixin/shareable_constant_value.rb new file mode 100644 index 00000000000..42f0c4325cd --- /dev/null +++ b/lib/rubocop/cop/mixin/shareable_constant_value.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + # Common functionality for dealing with shareable constant value. + module ShareableConstantValue + module_function + + LITERAL = :literal + EXPERIMENTAL_ANYTHING = :experimental_everything + + def shareable_constant_value? + target_ruby_version >= 3.0 && shareable_constant_value_comment_exists? + end + + def shareable_constant_value_comment_exists? + leading_comments.any? do |line| + shareable_constant_value = MagicComment.parse(line).shareable_constant_value + + [LITERAL, EXPERIMENTAL_ANYTHING].include?(shareable_constant_value) + end + end + + private + + def leading_comments + processed_source + .tokens + .take_while(&:comment?) + .map(&:text) + end + end + end +end diff --git a/lib/rubocop/cop/style/mutable_constant.rb b/lib/rubocop/cop/style/mutable_constant.rb index b2ff5658a83..87d33886c6d 100644 --- a/lib/rubocop/cop/style/mutable_constant.rb +++ b/lib/rubocop/cop/style/mutable_constant.rb @@ -54,6 +54,7 @@ module Style # end.freeze class MutableConstant < Base include FrozenStringLiteral + include ShareableConstantValue include ConfigurableEnforcedStyle extend AutoCorrector @@ -83,6 +84,7 @@ def on_assignment(value) end def strict_check(value) + return if shareable_constant_value? return if immutable_literal?(value) return if operation_produces_immutable_object?(value) return if frozen_string_literal?(value) @@ -93,6 +95,8 @@ def strict_check(value) end def check(value) + return if shareable_constant_value? + range_enclosed_in_parentheses = range_enclosed_in_parentheses?(value) return unless mutable_literal?(value) || diff --git a/lib/rubocop/magic_comment.rb b/lib/rubocop/magic_comment.rb index 20150a1c077..da99c4cc3ed 100644 --- a/lib/rubocop/magic_comment.rb +++ b/lib/rubocop/magic_comment.rb @@ -84,7 +84,13 @@ def frozen_string_literal # # @return [String] for shareable_constant_value config def shareable_constant_value - extract_shareable_constant_value + return unless (setting = extract_shareable_constant_value) + + case setting + when 'none' then nil + else + setting.to_sym + end end def encoding_specified? @@ -166,7 +172,7 @@ def extract_frozen_string_literal end def extract_shareable_constant_value - match('shareable[_-]constant[_-]values') + match('shareable[_-]constant[_-]value') end end diff --git a/spec/rubocop/cop/style/mutable_constant_spec.rb b/spec/rubocop/cop/style/mutable_constant_spec.rb index 800714b1e2d..c2cd08b08e9 100644 --- a/spec/rubocop/cop/style/mutable_constant_spec.rb +++ b/spec/rubocop/cop/style/mutable_constant_spec.rb @@ -157,6 +157,61 @@ RUBY end end + + context 'when using shareable_constant_value: literal' do + let(:prefix) { '# shareable_constant_value: literal' } + + it_behaves_like 'immutable objects', '[1, 2, 3]' + it_behaves_like 'immutable objects', '%w(a b c)' + it_behaves_like 'immutable objects', '{ a: 1, b: 2 }' + it_behaves_like 'immutable objects', "'str'" + it_behaves_like 'immutable objects', '"top#{1 + 2}"' + it_behaves_like 'immutable objects', '1' + it_behaves_like 'immutable objects', '1.5' + it_behaves_like 'immutable objects', ':sym' + it_behaves_like 'immutable objects', 'FOO + BAR' + it_behaves_like 'immutable objects', 'FOO - BAR' + it_behaves_like 'immutable objects', "'foo' + 'bar'" + it_behaves_like 'immutable objects', "ENV['foo']" + it_behaves_like 'immutable objects', "::ENV['foo']" + end + + context 'when using shareable_constant_value: experimental_everything' do + let(:prefix) { '# shareable_constant_value: experimental_everything' } + + it_behaves_like 'immutable objects', '[1, 2, 3]' + it_behaves_like 'immutable objects', '%w(a b c)' + it_behaves_like 'immutable objects', '{ a: 1, b: 2 }' + it_behaves_like 'immutable objects', "'str'" + it_behaves_like 'immutable objects', '"top#{1 + 2}"' + it_behaves_like 'immutable objects', '1' + it_behaves_like 'immutable objects', '1.5' + it_behaves_like 'immutable objects', ':sym' + it_behaves_like 'immutable objects', 'FOO + BAR' + it_behaves_like 'immutable objects', 'FOO - BAR' + it_behaves_like 'immutable objects', "'foo' + 'bar'" + it_behaves_like 'immutable objects', "ENV['foo']" + it_behaves_like 'immutable objects', "::ENV['foo']" + end + + context 'when using shareable_constant_value: none' do + let(:prefix) { '# shareable_constant_value: none' } + + it_behaves_like 'mutable objects', '[1, 2, 3]' + it_behaves_like 'mutable objects', '%w(a b c)' + it_behaves_like 'mutable objects', '{ a: 1, b: 2 }' + it_behaves_like 'mutable objects', "'str'" + it_behaves_like 'mutable objects', '"top#{1 + 2}"' + + it_behaves_like 'immutable objects', '1' + it_behaves_like 'immutable objects', '1.5' + it_behaves_like 'immutable objects', ':sym' + it_behaves_like 'immutable objects', 'FOO + BAR' + it_behaves_like 'immutable objects', 'FOO - BAR' + it_behaves_like 'immutable objects', "'foo' + 'bar'" + it_behaves_like 'immutable objects', "ENV['foo']" + it_behaves_like 'immutable objects', "::ENV['foo']" + end end context 'Ruby 2.7 or lower', :ruby27 do @@ -220,6 +275,44 @@ RUBY end end + + context 'when using shareable_constant_value: literal' do + let(:prefix) { '# shareable_constant_value: literal' } + + it_behaves_like 'mutable objects', '[1, 2, 3]' + it_behaves_like 'mutable objects', '%w(a b c)' + it_behaves_like 'mutable objects', '{ a: 1, b: 2 }' + it_behaves_like 'mutable objects', "'str'" + it_behaves_like 'mutable objects', '"top#{1 + 2}"' + + it_behaves_like 'immutable objects', '1' + it_behaves_like 'immutable objects', '1.5' + it_behaves_like 'immutable objects', ':sym' + it_behaves_like 'immutable objects', 'FOO + BAR' + it_behaves_like 'immutable objects', 'FOO - BAR' + it_behaves_like 'immutable objects', "'foo' + 'bar'" + it_behaves_like 'immutable objects', "ENV['foo']" + it_behaves_like 'immutable objects', "::ENV['foo']" + end + + context 'when using shareable_constant_value: experimental_everything' do + let(:prefix) { '# shareable_constant_value: experimental_everything' } + + it_behaves_like 'mutable objects', '[1, 2, 3]' + it_behaves_like 'mutable objects', '%w(a b c)' + it_behaves_like 'mutable objects', '{ a: 1, b: 2 }' + it_behaves_like 'mutable objects', "'str'" + it_behaves_like 'mutable objects', '"top#{1 + 2}"' + + it_behaves_like 'immutable objects', '1' + it_behaves_like 'immutable objects', '1.5' + it_behaves_like 'immutable objects', ':sym' + it_behaves_like 'immutable objects', 'FOO + BAR' + it_behaves_like 'immutable objects', 'FOO - BAR' + it_behaves_like 'immutable objects', "'foo' + 'bar'" + it_behaves_like 'immutable objects', "ENV['foo']" + it_behaves_like 'immutable objects', "::ENV['foo']" + end end context 'when the constant is a frozen string literal' do @@ -501,5 +594,162 @@ def assignment? it_behaves_like 'mutable objects', '"#{a}"' end + + context 'Ruby 3.0 or higher', :ruby30 do + context 'when using shareable_constant_value: literal' do + let(:prefix) { '# shareable_constant_value: literal' } + + it_behaves_like 'immutable objects', '[1, 2, 3]' + it_behaves_like 'immutable objects', '%w(a b c)' + it_behaves_like 'immutable objects', '{ a: 1, b: 2 }' + it_behaves_like 'immutable objects', "'str'" + it_behaves_like 'immutable objects', '"top#{1 + 2}"' + it_behaves_like 'immutable objects', 'Something.new' + it_behaves_like 'immutable objects', '1' + it_behaves_like 'immutable objects', '1.5' + it_behaves_like 'immutable objects', ':sym' + it_behaves_like 'immutable objects', "ENV['foo']" + it_behaves_like 'immutable objects', "::ENV['foo']" + it_behaves_like 'immutable objects', 'OTHER_CONST' + it_behaves_like 'immutable objects', '::OTHER_CONST' + it_behaves_like 'immutable objects', 'Namespace::OTHER_CONST' + it_behaves_like 'immutable objects', '::Namespace::OTHER_CONST' + it_behaves_like 'immutable objects', 'Struct.new' + it_behaves_like 'immutable objects', '::Struct.new' + it_behaves_like 'immutable objects', 'Struct.new(:a, :b)' + it_behaves_like 'immutable objects', <<~RUBY + Struct.new(:node) do + def assignment? + true + end + end + RUBY + end + + context 'when using shareable_constant_value: experimental_everything' do + let(:prefix) { '# shareable_constant_value: experimental_everything' } + + it_behaves_like 'immutable objects', '[1, 2, 3]' + it_behaves_like 'immutable objects', '%w(a b c)' + it_behaves_like 'immutable objects', '{ a: 1, b: 2 }' + it_behaves_like 'immutable objects', "'str'" + it_behaves_like 'immutable objects', '"top#{1 + 2}"' + it_behaves_like 'immutable objects', 'Something.new' + it_behaves_like 'immutable objects', '1' + it_behaves_like 'immutable objects', '1.5' + it_behaves_like 'immutable objects', ':sym' + it_behaves_like 'immutable objects', "ENV['foo']" + it_behaves_like 'immutable objects', "::ENV['foo']" + it_behaves_like 'immutable objects', 'OTHER_CONST' + it_behaves_like 'immutable objects', '::OTHER_CONST' + it_behaves_like 'immutable objects', 'Namespace::OTHER_CONST' + it_behaves_like 'immutable objects', '::Namespace::OTHER_CONST' + it_behaves_like 'immutable objects', 'Struct.new' + it_behaves_like 'immutable objects', '::Struct.new' + it_behaves_like 'immutable objects', 'Struct.new(:a, :b)' + it_behaves_like 'immutable objects', <<~RUBY + Struct.new(:node) do + def assignment? + true + end + end + RUBY + end + + context 'when using shareable_constant_value: none' do + let(:prefix) { '# shareable_constant_value: none' } + + it_behaves_like 'mutable objects', '[1, 2, 3]' + it_behaves_like 'mutable objects', '%w(a b c)' + it_behaves_like 'mutable objects', '{ a: 1, b: 2 }' + it_behaves_like 'mutable objects', "'str'" + it_behaves_like 'mutable objects', '"top#{1 + 2}"' + it_behaves_like 'mutable objects', 'Something.new' + + it_behaves_like 'immutable objects', '1' + it_behaves_like 'immutable objects', '1.5' + it_behaves_like 'immutable objects', ':sym' + it_behaves_like 'immutable objects', "ENV['foo']" + it_behaves_like 'immutable objects', "::ENV['foo']" + it_behaves_like 'immutable objects', 'OTHER_CONST' + it_behaves_like 'immutable objects', '::OTHER_CONST' + it_behaves_like 'immutable objects', 'Namespace::OTHER_CONST' + it_behaves_like 'immutable objects', '::Namespace::OTHER_CONST' + it_behaves_like 'immutable objects', 'Struct.new' + it_behaves_like 'immutable objects', '::Struct.new' + it_behaves_like 'immutable objects', 'Struct.new(:a, :b)' + it_behaves_like 'immutable objects', <<~RUBY + Struct.new(:node) do + def assignment? + true + end + end + RUBY + end + end + + context 'Ruby 2.7 or lower', :ruby27 do + context 'when using shareable_constant_value: literal' do + let(:prefix) { '# shareable_constant_value: literal' } + + it_behaves_like 'mutable objects', '[1, 2, 3]' + it_behaves_like 'mutable objects', '%w(a b c)' + it_behaves_like 'mutable objects', '{ a: 1, b: 2 }' + it_behaves_like 'mutable objects', "'str'" + it_behaves_like 'mutable objects', '"top#{1 + 2}"' + it_behaves_like 'mutable objects', 'Something.new' + + it_behaves_like 'immutable objects', '1' + it_behaves_like 'immutable objects', '1.5' + it_behaves_like 'immutable objects', ':sym' + it_behaves_like 'immutable objects', "ENV['foo']" + it_behaves_like 'immutable objects', "::ENV['foo']" + it_behaves_like 'immutable objects', 'OTHER_CONST' + it_behaves_like 'immutable objects', '::OTHER_CONST' + it_behaves_like 'immutable objects', 'Namespace::OTHER_CONST' + it_behaves_like 'immutable objects', '::Namespace::OTHER_CONST' + it_behaves_like 'immutable objects', 'Struct.new' + it_behaves_like 'immutable objects', '::Struct.new' + it_behaves_like 'immutable objects', 'Struct.new(:a, :b)' + it_behaves_like 'immutable objects', <<~RUBY + Struct.new(:node) do + def assignment? + true + end + end + RUBY + end + + context 'when using shareable_constant_value: experimental_everything' do + let(:prefix) { '# shareable_constant_value: experimental_everything' } + + it_behaves_like 'mutable objects', '[1, 2, 3]' + it_behaves_like 'mutable objects', '%w(a b c)' + it_behaves_like 'mutable objects', '{ a: 1, b: 2 }' + it_behaves_like 'mutable objects', "'str'" + it_behaves_like 'mutable objects', '"top#{1 + 2}"' + it_behaves_like 'mutable objects', 'Something.new' + + it_behaves_like 'immutable objects', '1' + it_behaves_like 'immutable objects', '1.5' + it_behaves_like 'immutable objects', ':sym' + it_behaves_like 'immutable objects', "ENV['foo']" + it_behaves_like 'immutable objects', "::ENV['foo']" + it_behaves_like 'immutable objects', 'OTHER_CONST' + it_behaves_like 'immutable objects', '::OTHER_CONST' + it_behaves_like 'immutable objects', 'Namespace::OTHER_CONST' + it_behaves_like 'immutable objects', '::Namespace::OTHER_CONST' + it_behaves_like 'immutable objects', 'Struct.new' + it_behaves_like 'immutable objects', '::Struct.new' + it_behaves_like 'immutable objects', 'Struct.new(:a, :b)' + it_behaves_like 'immutable objects', <<~RUBY + Struct.new(:node) do + def assignment? + true + end + end + RUBY + end + end end end diff --git a/spec/rubocop/magic_comment_spec.rb b/spec/rubocop/magic_comment_spec.rb index d81bc2056d9..ba10867ae13 100644 --- a/spec/rubocop/magic_comment_spec.rb +++ b/spec/rubocop/magic_comment_spec.rb @@ -4,6 +4,7 @@ shared_examples 'magic comment' do |comment, expectations = {}| encoding = expectations[:encoding] frozen_string = expectations[:frozen_string_literal] + shareable_constant_value = expectations[:shareable_constant_value] it "returns #{encoding.inspect} for encoding when comment is #{comment}" do expect(described_class.parse(comment).encoding).to eql(encoding) @@ -14,6 +15,12 @@ expect(described_class.parse(comment).frozen_string_literal) .to eql(frozen_string) end + + it "returns #{shareable_constant_value.inspect} for shareable_constant_value " \ + "when comment is #{comment}" do + expect(described_class.parse(comment).shareable_constant_value) + .to eql(shareable_constant_value) + end end include_examples 'magic comment', '#' @@ -74,22 +81,37 @@ frozen_string_literal: true include_examples 'magic comment', - '# -*- frozen-string-literal: true -*-', - frozen_string_literal: true + '# shareable_constant_value: none', + shareable_constant_value: nil + + include_examples 'magic comment', + '# shareable_constant_value: literal', + shareable_constant_value: :literal include_examples 'magic comment', - '# frozen_string_literal: invalid', - frozen_string_literal: 'invalid' + '# shareable_constant_value: experimental_everything', + shareable_constant_value: :experimental_everything + + include_examples 'magic comment', + '# shareable_constant_value: experimental_copy', + shareable_constant_value: :experimental_copy + + include_examples 'magic comment', + '# -*- frozen-string-literal: true -*-', + frozen_string_literal: true include_examples 'magic comment', '# -*- encoding : ascii-8bit -*-', encoding: 'ascii-8bit', - frozen_string_literal: nil + frozen_string_literal: nil, + shareable_constant_value: nil include_examples 'magic comment', - '# encoding: ascii-8bit frozen_string_literal: true', + '# encoding: ascii-8bit frozen_string_literal: true ' \ + 'shareable_constant_value: literal', encoding: 'ascii-8bit', - frozen_string_literal: nil + frozen_string_literal: nil, + shareable_constant_value: nil include_examples 'magic comment', '# frozen_string_literal: true encoding: ascii-8bit', @@ -103,23 +125,29 @@ include_examples( 'magic comment', - '# -*- encoding: ASCII-8BIT; frozen_string_literal: true -*-', + '# -*- encoding: ASCII-8BIT; frozen_string_literal: true; ' \ + 'shareable_constant_value: literal -*-', encoding: 'ascii-8bit', - frozen_string_literal: true + frozen_string_literal: true, + shareable_constant_value: :literal ) include_examples( 'magic comment', - '# coding: utf-8 -*- encoding: ASCII-8BIT; frozen_string_literal: true -*-', + '# coding: utf-8 -*- encoding: ASCII-8BIT; frozen_string_literal: true; ' \ + 'shareable_constant_value: literal -*-', encoding: 'ascii-8bit', - frozen_string_literal: true + frozen_string_literal: true, + shareable_constant_value: :literal ) include_examples( 'magic comment', - '# -*- coding: ASCII-8BIT; frozen_string_literal: true -*-', + '# -*- coding: ASCII-8BIT; frozen_string_literal: true; ' \ + 'shareable_constant_value: literal -*-', encoding: 'ascii-8bit', - frozen_string_literal: true + frozen_string_literal: true, + shareable_constant_value: :literal ) include_examples 'magic comment',