diff --git a/Library/Homebrew/style.rb b/Library/Homebrew/style.rb index 69a9f344abd3e..b17db3af0c09d 100644 --- a/Library/Homebrew/style.rb +++ b/Library/Homebrew/style.rb @@ -236,6 +236,15 @@ def self.run_rubocop(files, output_type, args << "--config" << (HOMEBREW_REPOSITORY/"docs/docs_rubocop_style.yml") elsif files.any? { |f| f.to_s.start_with? HOMEBREW_LIBRARY_PATH } base_dir = HOMEBREW_LIBRARY_PATH + elsif (tap = single_tap(files)) + # RuboCop roots its project index at the working directory for this + # config (see `tap_rubocop_style.yml`). A tap has to be indexed on its + # own: rooted at `Library`, the index spans every installed tap and the + # cross-file cops pair one tap's constants and methods with another's as + # reassignments and duplicates. The paths stay as given, so the shared + # config's `Taps/...` exclusions keep matching a symlinked tap. + args << "--config" << (HOMEBREW_LIBRARY/"tap_rubocop_style.yml") + base_dir = tap.path else args << "--config" << (HOMEBREW_LIBRARY/".rubocop.yml") base_dir = HOMEBREW_LIBRARY if files.any? { |f| f.to_s.start_with? HOMEBREW_LIBRARY } @@ -282,6 +291,15 @@ def self.run_rubocop(files, output_type, end end + sig { params(files: T::Array[Pathname]).returns(T.nilable(Tap)) } + def self.single_tap(files) + taps = files.filter_map { |file| Tap.from_path(file) } + return if taps.empty? || taps.length != files.length + return unless taps.map(&:name).uniq.one? + + taps.first + end + sig { params( files: T::Array[Pathname], diff --git a/Library/Homebrew/test/style_spec.rb b/Library/Homebrew/test/style_spec.rb index 4243bd47a33db..91e8057459ce3 100644 --- a/Library/Homebrew/test/style_spec.rb +++ b/Library/Homebrew/test/style_spec.rb @@ -256,5 +256,37 @@ class Test described_class.run_rubocop([ruby_file], :json, fix: true, todo: true) end + + it "roots RuboCop at the tap when every file is in one tap" do + tap_path = HOMEBREW_TAP_DIRECTORY/"homebrew/homebrew-foo" + script = tap_path/"cmd/foo.rb" + script.dirname.mkpath + script.write "# frozen_string_literal: true\n" + result = double(status: double(exitstatus: 0), stdout: '{"files":[]}') + + expect(described_class).to receive(:system_command).with( + anything, + hash_including(args: include("--config", HOMEBREW_LIBRARY/"tap_rubocop_style.yml", script), chdir: tap_path), + ).and_return(result) + + described_class.run_rubocop([script], :json) + end + + it "uses the shared config when files span multiple taps" do + scripts = %w[foo bar].map do |name| + script = HOMEBREW_TAP_DIRECTORY/"homebrew/homebrew-#{name}/cmd/#{name}.rb" + script.dirname.mkpath + script.write "# frozen_string_literal: true\n" + script + end + result = double(status: double(exitstatus: 0), stdout: '{"files":[]}') + + expect(described_class).to receive(:system_command).with( + anything, + hash_including(args: include("--config", HOMEBREW_LIBRARY/".rubocop.yml"), chdir: HOMEBREW_LIBRARY), + ).and_return(result) + + described_class.run_rubocop(scripts, :json) + end end end diff --git a/Library/tap_rubocop_style.yml b/Library/tap_rubocop_style.yml new file mode 100644 index 0000000000000..7e39385840b4f --- /dev/null +++ b/Library/tap_rubocop_style.yml @@ -0,0 +1,7 @@ +--- +# `brew style` passes this file as `--config` when every target is inside one +# tap. RuboCop roots its project index at the directory of a `.rubocop*` config +# file and at the working directory for any other name, so this file lets +# `brew style` root the index at the tap it is checking instead of at every +# tap under `Library/Taps`. +inherit_from: .rubocop.yml