diff --git a/config/default.yml b/config/default.yml index 2d81974f..65e6d238 100644 --- a/config/default.yml +++ b/config/default.yml @@ -1,3 +1,11 @@ +Sorbet/Refinement: + Description: >- + Checks for the use of Ruby Refinements library. Refinements add + complexity and incur a performance penalty that can be significant + for large code bases. They are also not supported by Sorbet. + Enabled: pending + VersionAdded: '<>' + inherit_mode: merge: - Exclude diff --git a/lib/rubocop/cop/sorbet/refinement.rb b/lib/rubocop/cop/sorbet/refinement.rb new file mode 100644 index 00000000..0f0217c9 --- /dev/null +++ b/lib/rubocop/cop/sorbet/refinement.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Sorbet + # Checks for the use of Ruby Refinements library. Refinements add + # complexity and incur a performance penalty that can be significant + # for large code bases. Good examples are cases of unrelated + # methods that happen to have the same name as these module methods. + # + # @example + # # bad + # module Foo + # refine(Date) do + # end + # end + # + # # bad + # module Foo + # using(Date) do + # end + # end + # + # # good + # module Foo + # bar.refine(Date) + # end + # + # # good + # module Foo + # bar.using(Date) + # end + + class Refinement < Base + MSG = "Do not use Ruby Refinements library as it is not supported by Sorbet." + RESTRICT_ON_SEND = [:refine, :using].freeze + + def on_send(node) + return unless node.receiver.nil? + return unless node.first_argument&.const_type? + + if node.method?(:refine) + return unless node.block_node + return unless node.parent.parent.module_type? + end + + add_offense(node) + end + end + end + end +end diff --git a/lib/rubocop/cop/sorbet_cops.rb b/lib/rubocop/cop/sorbet_cops.rb index 97e9d279..d5c1adfa 100644 --- a/lib/rubocop/cop/sorbet_cops.rb +++ b/lib/rubocop/cop/sorbet_cops.rb @@ -16,6 +16,7 @@ require_relative "sorbet/forbid_t_unsafe" require_relative "sorbet/forbid_t_untyped" require_relative "sorbet/redundant_extend_t_sig" +require_relative "sorbet/refinement" require_relative "sorbet/type_alias_name" require_relative "sorbet/obsolete_strict_memoization" require_relative "sorbet/buggy_obsolete_strict_memoization" diff --git a/manual/cops.md b/manual/cops.md index 73732fc8..1eae1758 100644 --- a/manual/cops.md +++ b/manual/cops.md @@ -33,6 +33,7 @@ In the following section you find all available cops: * [Sorbet/MultipleTEnumValues](cops_sorbet.md#sorbetmultipletenumvalues) * [Sorbet/ObsoleteStrictMemoization](cops_sorbet.md#sorbetobsoletestrictmemoization) * [Sorbet/RedundantExtendTSig](cops_sorbet.md#sorbetredundantextendtsig) +* [Sorbet/Refinement](cops_sorbet.md#sorbetrefinement) * [Sorbet/SignatureBuildOrder](cops_sorbet.md#sorbetsignaturebuildorder) * [Sorbet/SingleLineRbiClassModuleDefinitions](cops_sorbet.md#sorbetsinglelinerbiclassmoduledefinitions) * [Sorbet/StrictSigil](cops_sorbet.md#sorbetstrictsigil) diff --git a/manual/cops_sorbet.md b/manual/cops_sorbet.md index 17e724da..4c21e32d 100644 --- a/manual/cops_sorbet.md +++ b/manual/cops_sorbet.md @@ -759,6 +759,43 @@ class Example end ``` +## Sorbet/Refinement + +Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged +--- | --- | --- | --- | --- +Enabled | Yes | No | <> | - + +Checks for the use of Ruby Refinements library. Refinements add +complexity and incur a performance penalty that can be significant +for large code bases. Good examples are cases of unrelated +methods that happen to have the same name as these module methods. + +### Examples + +```ruby +# bad +module Foo + refine(Date) do + end +end + +# bad +module Foo + using(Date) do + end +end + +# good +module Foo + bar.refine(Date) +end + +# good +module Foo + bar.using(Date) +end +``` + ## Sorbet/SignatureBuildOrder Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged diff --git a/spec/rubocop/cop/sorbet/refinement_spec.rb b/spec/rubocop/cop/sorbet/refinement_spec.rb new file mode 100644 index 00000000..9b363b98 --- /dev/null +++ b/spec/rubocop/cop/sorbet/refinement_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +RSpec.describe(RuboCop::Cop::Sorbet::Refinement, :config) do + it "reports an offense for use of using" do + expect_offense(<<~RUBY, "my_class.rb") + using MyRefinement + ^^^^^^^^^^^^^^^^^^ Do not use Ruby Refinements library as it is not supported by Sorbet. + RUBY + end + + it "reports an offense for use of refine" do + expect_offense(<<~RUBY, "my_refinement.rb") + module MyRefinement + refine(String) do + ^^^^^^^^^^^^^^ Do not use Ruby Refinements library as it is not supported by Sorbet. + def to_s + "foo" + end + end + end + RUBY + end + + it "reports no offense for use of using with non-const argument" do + expect_no_offenses(<<~RUBY, "my_class.rb") + using "foo" + RUBY + end + + it "reports no offense for use of refine with non-const argument" do + expect_no_offenses(<<~RUBY, "my_refinement.rb") + module MyRefinement + refine "foo" do + def to_s + "foo" + end + end + end + RUBY + end + + it "reports no offense for use of refine with no block argument" do + expect_no_offenses(<<~RUBY, "my_refinement.rb") + module MyRefinement + refine(String) + end + RUBY + end + + it "reports no offense for use of refine outside of module" do + expect_no_offenses(<<~RUBY, "my_refinement.rb") + module MyNamespace + class MyClass + refine(String) do + def to_s + "foo" + end + end + end + end + RUBY + end +end