Merge pull request #13553 from max-ae/generate-completions-dsl-rubocop

rubocop: generate_completions DSL
This commit is contained in:
Rylan Polster 2022-09-06 12:09:46 -04:00 committed by GitHub
commit 89cb768055
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 298 additions and 0 deletions

View File

@ -415,6 +415,151 @@ module RuboCop
end
end
# This cop makes sure that the `generate_completions_from_executable` DSL is used.
#
# @api private
class GenerateCompletionsDSL < FormulaCop
extend AutoCorrector
def audit_formula(_node, _class_node, _parent_class_node, body_node)
install = find_method_def(body_node, :install)
return if install.blank?
correctable_shell_completion_node(install) do |node, shell, base_name, executable, subcmd, shell_parameter| # rubocop:disable Metrics/ParameterLists
# generate_completions_from_executable only applicable if shell is passed
next unless shell_parameter.match?(/(bash|zsh|fish)/)
base_name = base_name.delete_prefix("_").delete_suffix(".fish")
shell = shell.to_s.delete_suffix("_completion").to_sym
shell_parameter_stripped = shell_parameter
.delete_suffix("bash")
.delete_suffix("zsh")
.delete_suffix("fish")
shell_parameter_format = if shell_parameter_stripped.empty?
nil
elsif shell_parameter_stripped == "--"
:flag
elsif shell_parameter_stripped == "--shell="
:arg
else
shell_parameter_stripped
end
replacement_args = %w[]
replacement_args << executable.source
replacement_args << subcmd.source
replacement_args << "base_name: \"#{base_name}\"" unless base_name == @formula_name
replacement_args << "shells: [:#{shell}]"
unless shell_parameter_format.nil?
replacement_args << "shell_parameter_format: #{shell_parameter_format.inspect}"
end
offending_node(node)
replacement = "generate_completions_from_executable(#{replacement_args.join(", ")})"
problem "Use `#{replacement}` instead of `#{@offensive_node.source}`." do |corrector|
corrector.replace(@offensive_node.source_range, replacement)
end
end
shell_completion_node(install) do |node|
next if node.source.include?("<<~") # skip heredoc completion scripts
next if node.source.match?(/{.*=>.*}/) # skip commands needing custom ENV variables
offending_node(node)
problem "Use `generate_completions_from_executable` DSL instead of `#{@offensive_node.source}`."
end
end
# match ({bash,zsh,fish}_completion/"_?foo{.fish}?").write Utils.safe_popen_read(foo, subcmd, shell_parameter)
def_node_search :correctable_shell_completion_node, <<~EOS
$(send
(begin
(send
(send nil? ${:bash_completion :zsh_completion :fish_completion}) :/
(str $_))) :write
(send
(const nil? :Utils) :safe_popen_read
$(send
(send nil? :bin) :/
(str _))
$(str _)
(str $_)))
EOS
# matches ({bash,zsh,fish}_completion/"_?foo{.fish}?").write output
def_node_search :shell_completion_node, <<~EOS
$(send
(begin
(send
(send nil? {:bash_completion :zsh_completion :fish_completion}) :/
(str _))) :write _)
EOS
end
# This cop makes sure that the `generate_completions_from_executable` DSL is used with only
# a single, combined call for all shells.
#
# @api private
class SingleGenerateCompletionsDSLCall < FormulaCop
extend AutoCorrector
def audit_formula(_node, _class_node, _parent_class_node, body_node)
install = find_method_def(body_node, :install)
return if install.blank?
methods = find_every_method_call_by_name(install, :generate_completions_from_executable)
return if methods.length <= 1
offenses = []
shells = []
methods.each do |method|
next unless method.source.include?("shells:")
shells << method.source.match(/shells: \[(:bash|:zsh|:fish)\]/).captures.first
offenses << method
end
return if offenses.blank?
T.must(offenses[0...-1]).each_with_index do |node, i|
# commands have to be the same to be combined
# send_type? matches `bin/"foo"`, str_type? matches remaining command parts,
# the rest are kwargs we need to filter out
method_commands = node.arguments.filter { |arg| arg.send_type? || arg.str_type? }
next_method_commands = offenses[i + 1].arguments.filter { |arg| arg.send_type? || arg.str_type? }
unless method_commands == next_method_commands
shells.delete_at(i)
next
end
offending_node(node)
problem "Use a single `generate_completions_from_executable` " \
"call combining all specified shells." do |corrector|
# adjust range by -4 and +1 to also include & remove leading spaces and trailing \n
corrector.replace(@offensive_node.source_range.adjust(begin_pos: -4, end_pos: 1), "")
end
end
return if shells.length <= 1 # no shells to combine left
offending_node(offenses.last)
replacement = if (%w[:bash :zsh :fish] - shells).empty?
@offensive_node.source.sub(/shells: \[(:bash|:zsh|:fish)\]/, "")
.sub(", )", ")") # clean up dangling trailing comma
.sub("(, ", "(") # clean up dangling leading comma
.sub(", , ", ", ") # clean up dangling enclosed comma
else
@offensive_node.source.sub(/shells: \[(:bash|:zsh|:fish)\]/,
"shells: [#{shells.join(", ")}]")
end
problem "Use `#{replacement}` instead of `#{@offensive_node.source}`." do |corrector|
corrector.replace(@offensive_node.source_range, replacement)
end
end
end
# This cop checks for other miscellaneous style violations.
#
# @api private

View File

@ -5098,6 +5098,12 @@ class RuboCop::Cop::FormulaAudit::DeprecateDisableReason
def reason(param0); end
end
class RuboCop::Cop::FormulaAudit::GenerateCompletionsDSL
def correctable_shell_completion_node(param0); end
def shell_completion_node(param0); end
end
class RuboCop::Cop::FormulaAudit::GitUrls
def url_has_revision?(param0=T.unsafe(nil)); end
end

View File

@ -0,0 +1,147 @@
# typed: false
# frozen_string_literal: true
require "rubocops/lines"
describe RuboCop::Cop::FormulaAudit do
describe RuboCop::Cop::FormulaAudit::GenerateCompletionsDSL do
subject(:cop) { described_class.new }
it "reports an offense when writing to a shell completions file directly" do
expect_offense(<<~RUBY, "/homebrew-core/Formula/foo.rb")
class Foo < Formula
name "foo"
def install
(bash_completion/"foo").write Utils.safe_popen_read(bin/"foo", "completions", "bash")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `generate_completions_from_executable(bin/"foo", "completions", shells: [:bash])` instead of `(bash_completion/"foo").write Utils.safe_popen_read(bin/"foo", "completions", "bash")`.
end
end
RUBY
expect_correction(<<~RUBY)
class Foo < Formula
name "foo"
def install
generate_completions_from_executable(bin/"foo", "completions", shells: [:bash])
end
end
RUBY
end
it "reports an offense when writing to a shell completions file differing from the formula name" do
expect_offense(<<~RUBY, "/homebrew-core/Formula/foo-cli.rb")
class FooCli < Formula
name "foo-cli"
def install
(bash_completion/"foo").write Utils.safe_popen_read(bin/"foo", "completions", "bash")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `generate_completions_from_executable(bin/"foo", "completions", base_name: "foo", shells: [:bash])` instead of `(bash_completion/"foo").write Utils.safe_popen_read(bin/"foo", "completions", "bash")`.
end
end
RUBY
expect_correction(<<~RUBY)
class FooCli < Formula
name "foo-cli"
def install
generate_completions_from_executable(bin/"foo", "completions", base_name: "foo", shells: [:bash])
end
end
RUBY
end
it "reports an offense when writing to a shell completions file using an arg for the shell parameter" do
expect_offense(<<~RUBY, "/homebrew-core/Formula/foo.rb")
class Foo < Formula
name "foo"
def install
(bash_completion/"foo").write Utils.safe_popen_read(bin/"foo", "completions", "--shell=bash")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `generate_completions_from_executable(bin/"foo", "completions", shells: [:bash], shell_parameter_format: :arg)` instead of `(bash_completion/"foo").write Utils.safe_popen_read(bin/"foo", "completions", "--shell=bash")`.
end
end
RUBY
expect_correction(<<~RUBY)
class Foo < Formula
name "foo"
def install
generate_completions_from_executable(bin/"foo", "completions", shells: [:bash], shell_parameter_format: :arg)
end
end
RUBY
end
it "reports an offense when writing to a shell completions file using a custom flag for the shell parameter" do
expect_offense(<<~RUBY, "/homebrew-core/Formula/foo.rb")
class Foo < Formula
name "foo"
def install
(bash_completion/"foo").write Utils.safe_popen_read(bin/"foo", "completions", "--completion-script-bash")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `generate_completions_from_executable(bin/"foo", "completions", shells: [:bash], shell_parameter_format: "--completion-script-")` instead of `(bash_completion/"foo").write Utils.safe_popen_read(bin/"foo", "completions", "--completion-script-bash")`.
end
end
RUBY
expect_correction(<<~RUBY)
class Foo < Formula
name "foo"
def install
generate_completions_from_executable(bin/"foo", "completions", shells: [:bash], shell_parameter_format: "--completion-script-")
end
end
RUBY
end
it "reports an offense when writing to a completions file indirectly" do
expect_offense(<<~RUBY)
class Foo < Formula
name "foo"
def install
output = Utils.safe_popen_read(bin/"foo", "completions", "bash")
(bash_completion/"foo").write output
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `generate_completions_from_executable` DSL instead of `(bash_completion/"foo").write output`.
end
end
RUBY
end
end
describe RuboCop::Cop::FormulaAudit::SingleGenerateCompletionsDSLCall do
subject(:cop) { described_class.new }
it "reports an offense when using multiple #generate_completions_from_executable calls for different shells" do
expect_offense(<<~RUBY)
class Foo < Formula
name "foo"
def install
generate_completions_from_executable(bin/"foo", "completions", shells: [:bash])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use a single `generate_completions_from_executable` call combining all specified shells.
generate_completions_from_executable(bin/"foo", "completions", shells: [:zsh])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use a single `generate_completions_from_executable` call combining all specified shells.
generate_completions_from_executable(bin/"foo", "completions", shells: [:fish])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `generate_completions_from_executable(bin/"foo", "completions")` instead of `generate_completions_from_executable(bin/"foo", "completions", shells: [:fish])`.
end
end
RUBY
expect_correction(<<~RUBY)
class Foo < Formula
name "foo"
def install
generate_completions_from_executable(bin/"foo", "completions")
end
end
RUBY
end
end
end