diff --git a/Library/.rubocop.yml b/Library/.rubocop.yml index aaddb602a6ddc..829dd2dcdc50b 100644 --- a/Library/.rubocop.yml +++ b/Library/.rubocop.yml @@ -60,6 +60,11 @@ Homebrew/CompactBlank: # `blank?` is not necessarily available here: - "Homebrew/extend/enumerable.rb" +Homebrew/NoFileutilsRmrf: + Include: + - "/**/{Formula,Casks}/**/*.rb" + - "**/{Formula,Casks}/**/*.rb" + # only used internally Homebrew/MoveToExtendOS: Enabled: false diff --git a/Library/Homebrew/rubocops/all.rb b/Library/Homebrew/rubocops/all.rb index 284d135bd5970..bfcf79a8b9630 100644 --- a/Library/Homebrew/rubocops/all.rb +++ b/Library/Homebrew/rubocops/all.rb @@ -8,6 +8,7 @@ require_relative "io_read" require_relative "move_to_extend_os" require_relative "negate_include" +require_relative "no_fileutils_rmrf" require_relative "presence" require_relative "present" require_relative "safe_navigation_with_blank" diff --git a/Library/Homebrew/rubocops/no_fileutils_rmrf.rb b/Library/Homebrew/rubocops/no_fileutils_rmrf.rb new file mode 100644 index 0000000000000..b1e22cf30f9f9 --- /dev/null +++ b/Library/Homebrew/rubocops/no_fileutils_rmrf.rb @@ -0,0 +1,61 @@ +# typed: true +# frozen_string_literal: true + +module RuboCop + module Cop + module Homebrew + # This cop checks for the use of `FileUtils.rm_f`, `FileUtils.rm_rf`, or `{FileUtils,instance}.rmtree` + # and recommends the safer versions. + class NoFileutilsRmrf < Base + extend AutoCorrector + + MSG = "Use `rm` or `rm_r` instead of `rm_rf`, `rm_f`, or `rmtree`." + + def_node_matcher :any_receiver_rm_r_f?, <<~PATTERN + (send + {(const {nil? cbase} :FileUtils) (self)} + {:rm_rf :rm_f} + ...) + PATTERN + + def_node_matcher :no_receiver_rm_r_f?, <<~PATTERN + (send nil? {:rm_rf :rm_f} ...) + PATTERN + + def_node_matcher :no_receiver_rmtree?, <<~PATTERN + (send nil? :rmtree ...) + PATTERN + + def_node_matcher :any_receiver_rmtree?, <<~PATTERN + (send !nil? :rmtree ...) + PATTERN + + def on_send(node) + return if neither_rm_rf_nor_rmtree?(node) + + add_offense(node) do |corrector| + class_name = "FileUtils." if any_receiver_rm_r_f?(node) || any_receiver_rmtree?(node) + new_method = if node.method?(:rm_rf) || node.method?(:rmtree) + "rm_r" + else + "rm" + end + + args = if any_receiver_rmtree?(node) + node.receiver&.source || node.arguments.first&.source + else + node.arguments.first.source + end + args = "(#{args})" unless args.start_with?("(") + corrector.replace(node.loc.expression, "#{class_name}#{new_method}#{args}") + end + end + + def neither_rm_rf_nor_rmtree?(node) + !any_receiver_rm_r_f?(node) && !no_receiver_rm_r_f?(node) && + !any_receiver_rmtree?(node) && !no_receiver_rmtree?(node) + end + end + end + end +end diff --git a/Library/Homebrew/sorbet/rbi/dsl/rubo_cop/cop/homebrew/no_fileutils_rmrf.rbi b/Library/Homebrew/sorbet/rbi/dsl/rubo_cop/cop/homebrew/no_fileutils_rmrf.rbi new file mode 100644 index 0000000000000..b6d9c6a094994 --- /dev/null +++ b/Library/Homebrew/sorbet/rbi/dsl/rubo_cop/cop/homebrew/no_fileutils_rmrf.rbi @@ -0,0 +1,20 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for dynamic methods in `RuboCop::Cop::Homebrew::NoFileutilsRmrf`. +# Please instead update this file by running `bin/tapioca dsl RuboCop::Cop::Homebrew::NoFileutilsRmrf`. + + +class RuboCop::Cop::Homebrew::NoFileutilsRmrf + sig { params(node: RuboCop::AST::Node, kwargs: T.untyped, block: T.untyped).returns(T.untyped) } + def any_receiver_rm_r_f?(node, **kwargs, &block); end + + sig { params(node: RuboCop::AST::Node, kwargs: T.untyped, block: T.untyped).returns(T.untyped) } + def any_receiver_rmtree?(node, **kwargs, &block); end + + sig { params(node: RuboCop::AST::Node, kwargs: T.untyped, block: T.untyped).returns(T.untyped) } + def no_receiver_rm_r_f?(node, **kwargs, &block); end + + sig { params(node: RuboCop::AST::Node, kwargs: T.untyped, block: T.untyped).returns(T.untyped) } + def no_receiver_rmtree?(node, **kwargs, &block); end +end diff --git a/Library/Homebrew/test/rubocops/no_fileutils_rmrf_spec.rb b/Library/Homebrew/test/rubocops/no_fileutils_rmrf_spec.rb new file mode 100644 index 0000000000000..bc8b4aa99f6fa --- /dev/null +++ b/Library/Homebrew/test/rubocops/no_fileutils_rmrf_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require "rubocops/no_fileutils_rmrf" + +RSpec.describe RuboCop::Cop::Homebrew::NoFileutilsRmrf do + subject(:cop) { described_class.new } + + describe "rm_rf" do + it "registers an offense" do + expect_offense(<<~RUBY) + rm_rf("path/to/directory") + ^^^^^^^^^^^^^^^^^^^^^^^^^^ Homebrew/NoFileutilsRmrf: #{RuboCop::Cop::Homebrew::NoFileutilsRmrf::MSG} + FileUtils.rm_rf("path/to/directory") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Homebrew/NoFileutilsRmrf: #{RuboCop::Cop::Homebrew::NoFileutilsRmrf::MSG} + RUBY + end + + it "autocorrects" do + corrected = autocorrect_source(<<~RUBY) + rm_rf("path/to/directory") + FileUtils.rm_rf("path/to/other/directory") + RUBY + + expect(corrected).to eq(<<~RUBY) + rm_r("path/to/directory") + FileUtils.rm_r("path/to/other/directory") + RUBY + end + end + + describe "rm_f" do + it "registers an offense" do + expect_offense(<<~RUBY) + rm_f("path/to/directory") + ^^^^^^^^^^^^^^^^^^^^^^^^^ Homebrew/NoFileutilsRmrf: #{RuboCop::Cop::Homebrew::NoFileutilsRmrf::MSG} + FileUtils.rm_f("path/to/other/directory") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Homebrew/NoFileutilsRmrf: #{RuboCop::Cop::Homebrew::NoFileutilsRmrf::MSG} + RUBY + end + + it "autocorrects" do + corrected = autocorrect_source(<<~RUBY) + rm_f("path/to/directory") + FileUtils.rm_f("path/to/other/directory") + RUBY + + expect(corrected).to eq(<<~RUBY) + rm("path/to/directory") + FileUtils.rm("path/to/other/directory") + RUBY + end + end + + describe "rmtree" do + it "registers an offense" do + expect_offense(<<~RUBY) + rmtree("path/to/directory") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Homebrew/NoFileutilsRmrf: #{RuboCop::Cop::Homebrew::NoFileutilsRmrf::MSG} + other_dir = Pathname("path/to/other/directory") + other_dir.rmtree + ^^^^^^^^^^^^^^^^ Homebrew/NoFileutilsRmrf: #{RuboCop::Cop::Homebrew::NoFileutilsRmrf::MSG} + def buildpath + Pathname("path/to/yet/another/directory") + end + buildpath.rmtree + ^^^^^^^^^^^^^^^^ Homebrew/NoFileutilsRmrf: #{RuboCop::Cop::Homebrew::NoFileutilsRmrf::MSG} + (path/"here").rmtree + ^^^^^^^^^^^^^^^^^^^^ Homebrew/NoFileutilsRmrf: #{RuboCop::Cop::Homebrew::NoFileutilsRmrf::MSG} + RUBY + end + + it "autocorrects" do + corrected = autocorrect_source(<<~RUBY) + rmtree("path/to/directory") + other_dir = Pathname("path/to/other/directory") + other_dir.rmtree + def buildpath + Pathname("path/to/yet/another/directory") + end + buildpath.rmtree + (path/"here").rmtree + RUBY + + expect(corrected).to eq(<<~RUBY) + rm_r("path/to/directory") + other_dir = Pathname("path/to/other/directory") + FileUtils.rm_r(other_dir) + def buildpath + Pathname("path/to/yet/another/directory") + end + FileUtils.rm_r(buildpath) + FileUtils.rm_r(path/"here") + RUBY + end + end +end