Enable strict typing in Library/Homebrew/rubocops/

This commit is contained in:
Douglas Eichelberger
2026-03-30 20:04:55 -07:00
committed by Douglas Eichelberger
parent 3a946229d1
commit d03d51d887
29 changed files with 404 additions and 200 deletions
+2 -1
View File
@@ -66,5 +66,6 @@
}
},
"simplecov-vscode.path": "Library/Homebrew/test/coverage/.resultset.json",
"simplecov-vscode.enabled": false
"simplecov-vscode.enabled": false,
"specstory.cloudSync.enabled": "never"
}
+1 -1
View File
@@ -17,7 +17,7 @@ module RuboCop
sha256_nodes = find_method_calls_by_name(bottle_node.body, :sha256)
cellar_node = find_node_method_by_name(bottle_node.body, :cellar)
cellar_source = cellar_node&.first_argument&.source
cellar_source = T.cast(cellar_node, T.nilable(RuboCop::AST::SendNode))&.first_argument&.source
if sha256_nodes.present? && cellar_node.present?
offending_node(cellar_node)
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module RuboCop
@@ -35,6 +35,7 @@ module RuboCop
end
end
sig { params(source: T::Array[String]).returns(T::Array[String]) }
def sort_array(source)
# Combine each comment with the line(s) below so that they remain in the same relative location
combined_source = source.each_with_index.filter_map do |line, index|
@@ -50,17 +51,22 @@ module RuboCop
# Sort the lines that should be sorted
to_sort.sort! do |a, b|
a_non_comment = a.split("\n").reject { |line| line.strip.start_with?("#") }.first
b_non_comment = b.split("\n").reject { |line| line.strip.start_with?("#") }.first
a_non_comment.downcase <=> b_non_comment.downcase
a_non_comment = a.split("\n").reject { |line| line.strip.start_with?("#") }.fetch(0)
b_non_comment = b.split("\n").reject { |line| line.strip.start_with?("#") }.fetch(0)
a_non_comment.downcase <=> b_non_comment.downcase || raise("Expected non-comment lines to be present")
end
# Merge the sorted lines and the unsorted lines, preserving the original positions of the unsorted lines
combined_source.map { |line| to_keep.include?(line) ? line : to_sort.shift }
combined_source.map do |line|
next line if to_keep.include?(line)
to_sort.shift || raise("Expected to_sort to be present")
end
end
sig { params(source: T::Array[String], index: Integer, line: String).returns(String) }
def recursively_find_comments(source, index, line)
if source[index - 1].strip.start_with?("#")
if source.fetch(index - 1).strip.start_with?("#")
return recursively_find_comments(source, index - 1, "#{source[index - 1]}\n#{line}")
end
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module RuboCop
@@ -7,18 +7,22 @@ module RuboCop
# This class wraps the AST method node that represents the cask header. It
# includes various helper methods to aid cops in their analysis.
class CaskHeader
sig { params(method_node: T.all(RuboCop::AST::Node, RuboCop::AST::ParameterizedNode::RestArguments)).void }
def initialize(method_node)
@method_node = method_node
end
sig { returns(T.all(RuboCop::AST::Node, RuboCop::AST::ParameterizedNode::RestArguments)) }
attr_reader :method_node
sig { returns(String) }
def header_str
@header_str ||= source_range.source
@header_str ||= T.let(source_range.source, T.nilable(String))
end
sig { returns(Parser::Source::Range) }
def source_range
@source_range ||= method_node.loc.expression
@source_range ||= T.let(method_node.loc.expression, T.nilable(Parser::Source::Range))
end
sig { returns(String) }
@@ -26,16 +30,19 @@ module RuboCop
"cask '#{cask_token}'"
end
sig { returns(String) }
def cask_token
@cask_token ||= method_node.first_argument.str_content
@cask_token ||= T.let(method_node.first_argument.str_content, T.nilable(String))
end
sig { returns(T.all(RuboCop::AST::Node, RuboCop::AST::ParameterizedNode::RestArguments)) }
def hash_node
@hash_node ||= method_node.each_child_node(:hash).first
@hash_node ||= T.let(method_node.each_child_node(:hash).first, T.nilable(RuboCop::AST::Node))
end
sig { returns(T.all(RuboCop::AST::Node, RuboCop::AST::ParameterizedNode::RestArguments)) }
def pair_node
@pair_node ||= hash_node.each_child_node(:pair).first
@pair_node ||= T.let(hash_node.each_child_node(:pair).first, T.nilable(RuboCop::AST::Node))
end
end
end
+2 -2
View File
@@ -14,7 +14,7 @@ module RuboCop
sig {
params(
method_node: RuboCop::AST::Node,
method_node: T.any(RuboCop::AST::AsgnNode, RuboCop::AST::BlockNode, RuboCop::AST::SendNode),
all_comments: T::Array[T.any(String, Parser::Source::Comment)],
).void
}
@@ -23,7 +23,7 @@ module RuboCop
@all_comments = all_comments
end
sig { returns(RuboCop::AST::Node) }
sig { returns(T.any(RuboCop::AST::AsgnNode, RuboCop::AST::BlockNode, RuboCop::AST::SendNode)) }
attr_reader :method_node
alias stanza_node method_node
@@ -19,3 +19,8 @@ class RuboCop::AST::Node
sig { returns(T::Boolean) }
def begin_block?; end
end
class RuboCop::AST::BlockNode < RuboCop::AST::Node
sig { returns(RuboCop::AST::SendNode) }
def method_node; end
end
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "forwardable"
@@ -17,9 +17,10 @@ module RuboCop
MISSING_LINE_MSG = "stanza groups should be separated by a single empty line"
EXTRA_LINE_MSG = "stanzas within the same group should have no lines between them"
sig { override.params(cask_block: RuboCop::Cask::AST::CaskBlock).void }
def on_cask(cask_block)
@cask_block = cask_block
@line_ops = {}
@cask_block = T.let(cask_block, T.nilable(RuboCop::Cask::AST::CaskBlock))
@line_ops = T.let({}, T.nilable(T::Hash[Integer, Symbol]))
cask_stanzas = cask_block.toplevel_stanzas
add_offenses(cask_stanzas)
@@ -33,13 +34,15 @@ module RuboCop
private
attr_reader :cask_block, :line_ops
sig { returns(T.nilable(RuboCop::Cask::AST::CaskBlock)) }
attr_reader :cask_block
def_delegators :cask_block, :cask_node, :toplevel_stanzas
sig { params(stanzas: T::Array[RuboCop::Cask::AST::Stanza]).void }
def add_offenses(stanzas)
stanzas.each_cons(2) do |stanza, next_stanza|
next unless next_stanza
next if !stanza || !next_stanza
if missing_line_after?(stanza, next_stanza)
add_offense_missing_line(stanza)
@@ -49,28 +52,39 @@ module RuboCop
end
end
sig { returns(T::Hash[Integer, Symbol]) }
def line_ops
@line_ops || raise("Call to line_ops before it has been initialized")
end
sig { params(stanza: RuboCop::Cask::AST::Stanza, next_stanza: RuboCop::Cask::AST::Stanza).returns(T::Boolean) }
def missing_line_after?(stanza, next_stanza)
!(stanza.same_group?(next_stanza) ||
empty_line_after?(stanza))
end
sig { params(stanza: RuboCop::Cask::AST::Stanza, next_stanza: RuboCop::Cask::AST::Stanza).returns(T::Boolean) }
def extra_line_after?(stanza, next_stanza)
stanza.same_group?(next_stanza) &&
empty_line_after?(stanza)
end
sig { params(stanza: RuboCop::Cask::AST::Stanza).returns(T::Boolean) }
def empty_line_after?(stanza)
source_line_after(stanza).empty?
end
sig { params(stanza: RuboCop::Cask::AST::Stanza).returns(String) }
def source_line_after(stanza)
processed_source[index_of_line_after(stanza)]
end
sig { params(stanza: RuboCop::Cask::AST::Stanza).returns(Integer) }
def index_of_line_after(stanza)
stanza.source_range.last_line
end
sig { params(stanza: RuboCop::Cask::AST::Stanza).void }
def add_offense_missing_line(stanza)
line_index = index_of_line_after(stanza)
line_ops[line_index] = :insert
@@ -79,6 +93,7 @@ module RuboCop
end
end
sig { params(stanza: RuboCop::Cask::AST::Stanza).void }
def add_offense_extra_line(stanza)
line_index = index_of_line_after(stanza)
line_ops[line_index] = :remove
@@ -87,11 +102,14 @@ module RuboCop
end
end
def add_offense(line_index, message:)
sig { params(line_index: Integer, message: String, block: T.proc.params(corrector: RuboCop::Cop::Corrector).void).void }
def add_offense(line_index, message:, &block)
line_length = [processed_source[line_index].size, 1].max
@range = source_range(processed_source.buffer, line_index + 1, 0,
line_length)
super(@range, message:)
@range = T.let(
source_range(processed_source.buffer, line_index + 1, 0, line_length),
T.nilable(Parser::Source::Range),
)
super(@range, message:, &block)
end
end
end
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "forwardable"
@@ -15,6 +15,7 @@ module RuboCop
MESSAGE = "`%<stanza>s` stanza out of order"
sig { override.params(stanza_block: RuboCop::Cask::AST::StanzaBlock).void }
def on_cask_stanza_block(stanza_block)
stanzas = stanza_block.stanzas
ordered_stanzas = sort_stanzas(stanzas)
@@ -29,6 +30,7 @@ module RuboCop
message: format(MESSAGE, stanza: stanza_before.stanza_name),
) do |corrector|
next if part_of_ignored_node?(stanza_before.method_node)
raise "unexpected nil value for stanza_after" unless stanza_after
corrector.replace(
stanza_before.source_range_with_comments,
@@ -41,6 +43,7 @@ module RuboCop
end
end
sig { override.void }
def on_new_investigation
super
@@ -49,6 +52,7 @@ module RuboCop
private
sig { params(stanzas: T::Array[RuboCop::Cask::AST::Stanza]).returns(T::Array[RuboCop::Cask::AST::Stanza]) }
def sort_stanzas(stanzas)
stanzas.sort do |stanza1, stanza2|
i1 = stanza1.stanza_index
@@ -58,15 +62,12 @@ module RuboCop
i1 = stanzas.index(stanza1)
i2 = stanzas.index(stanza2)
end
raise "unexpected nil value for i1" unless i1
raise "unexpected nil value for i2" unless i2
i1 - i2
end
end
def stanza_order_index(stanza)
stanza_name = stanza.respond_to?(:method_name) ? stanza.method_name : stanza.stanza_name
RuboCop::Cask::Constants::STANZA_ORDER.index(stanza_name)
end
end
end
end
+1 -1
View File
@@ -37,7 +37,7 @@ module RuboCop
url_stanza = stanza_node.first_argument
hash_node = stanza_node.last_argument
audit_url(:cask, [stanza.stanza_node], [], livecheck_urls: [])
audit_url(:cask, [stanza_node], [], livecheck_urls: [])
# Check for http:// URLs in homebrew-cask (skip deprecated/disabled casks)
# TODO: Remove the deprecated/disabled check after Homebrew/cask has no more
+35 -6
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "ast_constants"
@@ -14,13 +14,20 @@ module RuboCop
class ComponentsOrder < FormulaCop
extend AutoCorrector
sig { params(config: T.nilable(RuboCop::Config), options: T.nilable(T::Hash[Symbol, T.anything])).void }
def initialize(config = nil, options = nil)
super
@present_components = T.let(nil, T.nilable(T::Array[T::Array[RuboCop::AST::Node]]))
@offensive_nodes = T.let(nil, T.nilable(T::Array[RuboCop::AST::Node]))
end
sig { override.params(formula_nodes: FormulaNodes).void }
def audit_formula(formula_nodes)
return if (body_node = formula_nodes.body_node).nil?
@present_components, @offensive_nodes = check_order(FORMULA_COMPONENT_PRECEDENCE_LIST, body_node)
component_problem @offensive_nodes[0], @offensive_nodes[1] if @offensive_nodes
component_problem @offensive_nodes.fetch(0), @offensive_nodes.fetch(1) if @offensive_nodes
component_precedence_list = [
[{ name: :depends_on, type: :method_call }],
@@ -42,7 +49,7 @@ module RuboCop
problem "There can only be one `#{on_method}` block in a formula."
end
check_on_system_block_content(component_precedence_list, on_method_blocks.first)
check_on_system_block_content(component_precedence_list, on_method_blocks.fetch(0))
end
resource_blocks = find_blocks(body_node, :resource)
@@ -119,11 +126,23 @@ module RuboCop
end
end
sig {
params(
component_precedence_list: T::Array[T::Array[{ name: Symbol, type: Symbol }]],
block: RuboCop::AST::BlockNode,
).void
}
def check_block_component_order(component_precedence_list, block)
@present_components, offensive_node = check_order(component_precedence_list, block.body)
component_problem(*offensive_node) if offensive_node
end
sig {
params(
component_precedence_list: T::Array[T::Array[{ name: Symbol, type: Symbol }]],
on_system_block: RuboCop::AST::BlockNode,
).void
}
def check_on_system_block_content(component_precedence_list, on_system_block)
if on_system_block.body.block_type? && !on_system_methods.include?(on_system_block.body.method_name) &&
on_system_block.body.method_name != :fails_with
@@ -171,6 +190,7 @@ module RuboCop
# Reorder two nodes in the source, using the corrector instance in autocorrect method.
# Components of same type are grouped together when rewriting the source.
# Linebreaks are introduced if components are of two different methods/blocks/multilines.
sig { params(corrector: RuboCop::Cop::Corrector, node1: RuboCop::AST::Node, node2: RuboCop::AST::Node).void }
def reorder_components(corrector, node1, node2)
# order_idx : node1's index in component_precedence_list
# curr_p_idx: node1's index in preceding_comp_arr
@@ -179,7 +199,7 @@ module RuboCop
# curr_p_idx.positive? means node1 needs to be grouped with its own kind
if curr_p_idx.positive?
node2 = preceding_comp_arr[curr_p_idx - 1]
node2 = preceding_comp_arr.fetch(curr_p_idx - 1)
indentation = " " * (start_column(node2) - line_start_column(node2))
line_breaks = node2.multiline? ? "\n\n" : "\n"
corrector.insert_after(node2.source_range, line_breaks + indentation + node1.source)
@@ -193,12 +213,20 @@ module RuboCop
end
# Returns precedence index and component's index to properly reorder and group during autocorrect.
sig { params(node1: RuboCop::AST::Node).returns([Integer, Integer, T::Array[RuboCop::AST::Node]]) }
def get_state(node1)
@present_components.each_with_index do |comp, idx|
return [idx, comp.index(node1), comp] if comp.member?(node1)
T.must(@present_components).each_with_index do |comp, idx|
return [idx, T.must(comp.index(node1)), comp] if comp.member?(node1)
end
raise "Could not find node1 in present_components"
end
sig {
params(
component_precedence_list: T::Array[T::Array[{ name: Symbol, type: Symbol }]],
body_node: RuboCop::AST::Node,
).returns(T.nilable([T::Array[T::Array[RuboCop::AST::Node]], T::Array[RuboCop::AST::Node]]))
}
def check_order(component_precedence_list, body_node)
present_components = component_precedence_list.map do |components|
components.flat_map do |component|
@@ -229,6 +257,7 @@ module RuboCop
end
# Method to report and correct component precedence violations.
sig { params(component1: RuboCop::AST::Node, component2: RuboCop::AST::Node).void }
def component_problem(component1, component2)
return if tap_style_exception? :components_order_exceptions
+3 -3
View File
@@ -20,16 +20,16 @@ module RuboCop
find_method_calls_by_name(body_node, :conflicts_with).each do |conflicts_with_call|
next unless parameters(conflicts_with_call).last.respond_to? :values
reason = parameters(conflicts_with_call).last.values.first
reason = T.cast(parameters(conflicts_with_call).fetch(-1), RuboCop::AST::HashNode).values.first
offending_node(reason)
name = Regexp.new(T.must(@formula_name), Regexp::IGNORECASE)
reason_text = string_content(reason).sub(name, "")
first_word = reason_text.split.first
first_word = reason_text.split.fetch(0)
if reason_text.match?(/\A[A-Z]/)
problem "'#{first_word}' from the `conflicts_with` reason " \
"should be '#{first_word.downcase}'." do |corrector|
reason_text[0] = reason_text[0].downcase
reason_text[0] = T.must(reason_text[0]).downcase
corrector.replace(reason.source_range, "\"#{reason_text}\"")
end
end
+20 -5
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "rubocops/extend/formula_cop"
@@ -28,6 +28,7 @@ module RuboCop
end
end
sig { params(parent_node: T.nilable(RuboCop::AST::Node)).void }
def check_uses_from_macos_nodes_order(parent_node)
return if parent_node.nil?
@@ -35,6 +36,7 @@ module RuboCop
ensure_dependency_order(dependency_nodes)
end
sig { params(parent_node: T.nilable(RuboCop::AST::Node)).void }
def check_dependency_nodes_order(parent_node)
return if parent_node.nil?
@@ -42,9 +44,14 @@ module RuboCop
ensure_dependency_order(dependency_nodes)
end
sig { params(nodes: T::Array[RuboCop::AST::Node]).void }
def ensure_dependency_order(nodes)
name_node_pairs = nodes.map { |node| [dependency_name(node), node] }
name_node_pairs.select! { |name, _| name } # skip nodes with invalid dependency name
name_node_pairs = nodes.filter_map do |node|
name = dependency_name(node)
next unless name
[name, node]
end
name_node_pairs.sort_by! { |name, _| name.downcase }
ordered = sort_dependencies_by_type(name_node_pairs.map { |_, node| node })
sort_conditional_dependencies!(ordered)
@@ -53,6 +60,7 @@ module RuboCop
# Separate dependencies according to precedence order:
# build-time > test > normal > recommended > optional
sig { params(dependency_nodes: T::Array[RuboCop::AST::Node]).returns(T::Array[RuboCop::AST::Node]) }
def sort_dependencies_by_type(dependency_nodes)
unsorted_deps = dependency_nodes.to_a
ordered = []
@@ -90,13 +98,14 @@ module RuboCop
end
break if idx2
end
insert_after!(ordered, idx1, idx2 + T.must(idx1)) if idx2
insert_after!(ordered, idx1, idx2 + idx1) if idx1 &&idx2
end
ordered
end
# Verify actual order of sorted `depends_on` nodes in source code;
# raise RuboCop problem otherwise.
sig { params(ordered: T::Array[RuboCop::AST::Node]).void }
def verify_order_in_source(ordered)
ordered.each_with_index do |node_1, idx|
l1 = line_number(node_1)
@@ -154,16 +163,22 @@ module RuboCop
(send (send nil? :build) :with? $({str sym} _))
EOS
sig { params(arr: T::Array[RuboCop::AST::Node], idx1: Integer, idx2: Integer).void }
def insert_after!(arr, idx1, idx2)
arr.insert(idx2+1, arr.delete_at(idx1))
arr.insert(
idx2+1,
arr.delete_at(idx1) || raise("unexpected nil value for arr.delete_at(idx1)"),
)
end
sig { params(node: RuboCop::AST::Node).returns(T.nilable(T::Array[String])) }
def build_with_dependency_name(node)
match_nodes = build_with_dependency_node(node)
match_nodes = match_nodes.to_a.compact
match_nodes.map { |n| string_content(n) } unless match_nodes.empty?
end
sig { params(dependency_node: RuboCop::AST::Node).returns(T.nilable(String)) }
def dependency_name(dependency_node)
match_node = dependency_name_node(dependency_node).to_a.first
string_content(match_node) if match_node
@@ -23,7 +23,7 @@ module RuboCop
Date.iso8601(string_content(date_node))
rescue ArgumentError
fixed_date_string = Date.parse(string_content(date_node)).iso8601
offending_node(date_node)
@offensive_node = date_node
problem "Use `#{fixed_date_string}` to comply with ISO 8601" do |corrector|
corrector.replace(date_node.source_range, "\"#{fixed_date_string}\"")
end
@@ -56,18 +56,18 @@ module RuboCop
reason_found = true
next if reason_node.sym_type?
offending_node(reason_node)
@offensive_node = reason_node
reason_string = string_content(reason_node)
if reason_string.start_with?("it ")
problem "Do not start the reason with `it`" do |corrector|
corrector.replace(T.must(@offensive_node).source_range, "\"#{reason_string[3..]}\"")
corrector.replace(@offensive_node.source_range, "\"#{reason_string[3..]}\"")
end
end
if PUNCTUATION_MARKS.include?(reason_string[-1])
problem "Do not end the reason with a punctuation mark" do |corrector|
corrector.replace(T.must(@offensive_node).source_range, "\"#{reason_string.chop}\"")
corrector.replace(@offensive_node.source_range, "\"#{reason_string.chop}\"")
end
end
end
@@ -51,12 +51,12 @@ module RuboCop
sig {
params(
urls: T::Array[RuboCop::AST::Node], regex: Regexp,
_block: T.proc.params(arg0: T::Array[RuboCop::AST::Node], arg1: String, arg2: Integer).void
_block: T.proc.params(arg0: MatchData, arg1: String, arg2: Integer).void
).void
}
def audit_urls(urls, regex, &_block)
urls.each_with_index do |url_node, index|
url_string_node = parameters(url_node).first
url_string_node = parameters(url_node).fetch(0)
url_string = string_content(url_string_node)
match_object = regex_match_group(url_string_node, regex)
next unless match_object
@@ -145,20 +145,21 @@ module RuboCop
def get_checksum_node(call)
return if parameters(call).empty? || parameters(call).nil?
if parameters(call).first.str_type?
if parameters(call).fetch(0).str_type?
parameters(call).first
# sha256 is passed as a key-value pair in bottle blocks
elsif parameters(call).first.hash_type?
if parameters(call).first.keys.first.value == :cellar
elsif parameters(call).fetch(0).hash_type?
hash_node = T.cast(parameters(call).fetch(0), RuboCop::AST::HashNode)
if hash_node.keys.first.value == :cellar
# sha256 :cellar :any, :tag "hexdigest"
parameters(call).first.values.last
elsif parameters(call).first.keys.first.is_a?(RuboCop::AST::SymbolNode)
hash_node.values.last
elsif hash_node.keys.first.is_a?(RuboCop::AST::SymbolNode)
# sha256 :tag "hexdigest"
parameters(call).first.values.first
hash_node.values.first
else
# Legacy bottle block syntax
# sha256 "hexdigest" => :tag
parameters(call).first.keys.first
hash_node.keys.first
end
end
end
+1 -1
View File
@@ -23,7 +23,7 @@ module RuboCop
return
end
homepage_parameter_node = parameters(homepage_node).first
homepage_parameter_node = parameters(homepage_node).fetch(0)
offending_node(homepage_parameter_node)
content = string_content(homepage_parameter_node)
+9 -7
View File
@@ -27,23 +27,23 @@ module RuboCop
Firefox
].freeze
reason = parameters(keg_only_node).first
offending_node(reason)
reason = parameters(keg_only_node).fetch(0)
@offensive_node = reason
name = Regexp.new(T.must(@formula_name), Regexp::IGNORECASE)
reason = string_content(reason).sub(name, "")
first_word = reason.split.first
first_word = reason.split.fetch(0)
if /\A[A-Z]/.match?(reason) && !reason.start_with?(*allowlist)
problem "'#{first_word}' from the `keg_only` reason should be '#{first_word.downcase}'." do |corrector|
reason[0] = reason[0].downcase
corrector.replace(T.must(@offensive_node).source_range, "\"#{reason}\"")
reason[0] = T.must(reason[0]).downcase # reason[0] must exist because of the regexp match
corrector.replace(@offensive_node.source_range, "\"#{reason}\"")
end
end
return unless reason.end_with?(".")
problem "`keg_only` reason should not end with a period." do |corrector|
corrector.replace(T.must(@offensive_node).source_range, "\"#{reason.chop}\"")
corrector.replace(@offensive_node.source_range, "\"#{reason.chop}\"")
end
end
@@ -51,7 +51,9 @@ module RuboCop
def autocorrect(node)
lambda do |corrector|
reason = string_content(node)
reason[0] = reason[0].downcase
raise "unexpected empty reason" unless reason[0]
reason[0] = T.must(reason[0]).downcase # reason[0] must exist because of the previous line
reason = reason.delete_suffix(".")
corrector.replace(node.source_range, "\"#{reason}\"")
end
+57 -50
View File
@@ -39,8 +39,10 @@ module RuboCop
end_pos = end_column(formula_nodes.class_node)
return if begin_pos-end_pos == 3
raise "unexpected nil value for @formula_name" unless @formula_name
problem "Use a space in class inheritance: " \
"class #{T.must(@formula_name).capitalize} < #{class_name(parent_class_node)}"
"class #{@formula_name.capitalize} < #{class_name(parent_class_node)}"
end
end
@@ -112,25 +114,25 @@ module RuboCop
find_every_method_call_by_name(body_node, :assert_predicate).each do |method|
args = parameters(method)
next if args[1].source != ":exist?"
next if args.fetch(1).source != ":exist?"
offending_node(method)
@offensive_node = method
problem "Use `assert_path_exists <path_to_file>` instead of `#{method.source}`" do |corrector|
correct = "assert_path_exists #{args.first.source}"
correct += ", #{args[2].source}" if args.length == 3
corrector.replace(T.must(@offensive_node).source_range, correct)
correct = "assert_path_exists #{args.fetch(0).source}"
correct += ", #{args.fetch(2).source}" if args.length == 3
corrector.replace(@offensive_node.source_range, correct)
end
end
find_every_method_call_by_name(body_node, :refute_predicate).each do |method|
args = parameters(method)
next if args[1].source != ":exist?"
next if args.fetch(1).source != ":exist?"
offending_node(method)
@offensive_node = method
problem "Use `refute_path_exists <path_to_file>` instead of `#{method.source}`" do |corrector|
correct = "refute_path_exists #{args.first.source}"
correct += ", #{args[2].source}" if args.length == 3
corrector.replace(T.must(@offensive_node).source_range, correct)
correct = "refute_path_exists #{args.fetch(0).source}"
correct += ", #{args.fetch(2).source}" if args.length == 3
corrector.replace(@offensive_node.source_range, correct)
end
end
end
@@ -188,7 +190,7 @@ module RuboCop
end
find_instance_method_call(body_node, :build, :without?) do |method|
arg = parameters(method).first
arg = parameters(method).fetch(0)
next unless (match = regex_match_group(arg, /^-?-?without-(.*)/))
problem "Instead of duplicating `without`, " \
@@ -196,7 +198,7 @@ module RuboCop
end
find_instance_method_call(body_node, :build, :with?) do |method|
arg = parameters(method).first
arg = parameters(method).fetch(0)
next unless (match = regex_match_group(arg, /^-?-?with-(.*)/))
problem "Instead of duplicating `with`, " \
@@ -262,7 +264,7 @@ module RuboCop
find_method_with_args(body_node, :std_npm_install_args) do |method|
problem "Use `std_npm_args` instead of `#{T.cast(@offensive_node,
RuboCop::AST::SendNode).method_name}`." do |corrector|
if (param = parameters(method).first.source) == "libexec"
if (param = parameters(method).fetch(0).source) == "libexec"
corrector.replace(T.must(@offensive_node).source_range, "std_npm_args")
else
corrector.replace(T.must(@offensive_node).source_range, "std_npm_args(prefix: #{param})")
@@ -342,7 +344,7 @@ module RuboCop
next if dependency.empty?
next unless dependency.end_with?("-full")
offending_node(node)
@offensive_node = node
problem "Formulae in homebrew/core should not depend on `#{dependency}`."
break
end
@@ -399,7 +401,7 @@ module RuboCop
popen_commands.each do |command|
find_instance_method_call(body_node, "Utils", command) do |method|
next unless (match = regex_match_group(parameters(method).first, /^([^"' ]+)=([^"' ]+)(?: (.*))?$/))
next unless (match = regex_match_group(parameters(method).fetch(0), /^([^"' ]+)=([^"' ]+)(?: (.*))?$/))
good_args = "Utils.#{command}({ \"#{match[1]}\" => \"#{match[2]}\" }, \"#{match[3]}\")"
@@ -422,11 +424,12 @@ module RuboCop
license_node = find_node_method_by_name(body_node, :license)
return unless license_node
license = parameters(license_node).first
license = parameters(license_node).fetch(0)
return unless license.array_type?
problem "Use `license any_of: #{license.source}` instead of `license #{license.source}`" do |corrector|
corrector.replace(license_node.source_range, "license any_of: #{parameters(license_node).first.source}")
corrector.replace(license_node.source_range,
"license any_of: #{parameters(license_node).fetch(0).source}")
end
end
end
@@ -441,7 +444,7 @@ module RuboCop
return unless license_node
return if license_node.source.include?("\n")
parameters(license_node).first.each_descendant(:hash).each do |license_hash|
parameters(license_node).fetch(0).each_descendant(:hash).each do |license_hash|
next if license_exception? license_hash
problem "Split nested license declarations onto multiple lines"
@@ -462,19 +465,20 @@ module RuboCop
return if (body_node = formula_nodes.body_node).nil?
python_formula_node = find_every_method_call_by_name(body_node, :depends_on).find do |dep|
string_content(parameters(dep).first).start_with? "python@"
string_content(parameters(dep).fetch(0)).start_with? "python@"
end
python_version = if python_formula_node.blank?
other_python_nodes = find_every_method_call_by_name(body_node, :depends_on).select do |dep|
parameters(dep).first.instance_of?(RuboCop::AST::HashNode) &&
string_content(parameters(dep).first.keys.first).start_with?("python@")
first_param = parameters(dep).first
first_param.instance_of?(RuboCop::AST::HashNode) &&
string_content(first_param.keys.first).start_with?("python@")
end
return if other_python_nodes.size != 1
string_content(parameters(other_python_nodes.first).first.keys.first).split("@").last
string_content(T.cast(parameters(other_python_nodes.fetch(0)).fetch(0), RuboCop::AST::HashNode).keys.first).split("@").last
else
string_content(parameters(python_formula_node).first).split("@").last
string_content(parameters(python_formula_node).fetch(0)).split("@").last
end
find_strings(body_node).each do |str|
@@ -596,11 +600,11 @@ module RuboCop
replacement_args << "shell_parameter_format: #{shell_parameter_format.inspect}"
end
offending_node(node)
@offensive_node = node
replacement = "generate_completions_from_executable(#{replacement_args.join(", ")})"
problem "Use `#{replacement}` instead of `#{T.must(@offensive_node).source}`." do |corrector|
corrector.replace(T.must(@offensive_node).source_range, replacement)
problem "Use `#{replacement}` instead of `#{@offensive_node.source}`." do |corrector|
corrector.replace(@offensive_node.source_range, replacement)
end
end
@@ -674,30 +678,30 @@ module RuboCop
next
end
offending_node(node)
@offensive_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(T.must(@offensive_node).source_range.adjust(begin_pos: -4, end_pos: 1), "")
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)
@offensive_node = offenses.fetch(-1)
replacement = if (%w[:bash :zsh :fish] - shells).empty?
T.must(@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
@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
T.must(@offensive_node).source.sub(/shells: \[(:bash|:zsh|:fish)\]/,
"shells: [#{shells.join(", ")}]")
@offensive_node.source.sub(/shells: \[(:bash|:zsh|:fish)\]/,
"shells: [#{shells.join(", ")}]")
end
problem "Use `#{replacement}` instead of `#{T.must(@offensive_node).source}`." do |corrector|
corrector.replace(T.must(@offensive_node).source_range, replacement)
problem "Use `#{replacement}` instead of `#{@offensive_node.source}`." do |corrector|
corrector.replace(@offensive_node.source_range, replacement)
end
end
end
@@ -733,14 +737,14 @@ module RuboCop
end
find_instance_method_call(body_node, :man, :+) do |method|
next unless (match = regex_match_group(parameters(method).first, /^man[1-8]$/))
next unless (match = regex_match_group(parameters(method).fetch(0), /^man[1-8]$/))
problem "`#{method.source}` should be `#{match[0]}`"
end
# Avoid hard-coding compilers
find_every_method_call_by_name(body_node, :system).each do |method|
param = parameters(method).first
param = parameters(method).fetch(0)
if (match = regex_match_group(param, %r{^(/usr/bin/)?(gcc|clang|cc|c[89]9)(\s|$)}))
problem "Use `\#{ENV.cc}` instead of hard-coding `#{match[2]}`"
elsif (match = regex_match_group(param, %r{^(/usr/bin/)?((g|clang|c)\+\+)(\s|$)}))
@@ -749,7 +753,7 @@ module RuboCop
end
find_instance_method_call(body_node, "ENV", :[]=) do |method|
param = parameters(method)[1]
param = parameters(method).fetch(1)
if (match = regex_match_group(param, %r{^(/usr/bin/)?(gcc|clang|cc|c[89]9)(\s|$)}))
problem "Use `\#{ENV.cc}` instead of hard-coding `#{match[2]}`"
elsif (match = regex_match_group(param, %r{^(/usr/bin/)?((g|clang|c)\+\+)(\s|$)}))
@@ -772,12 +776,13 @@ module RuboCop
problem ["`#", "{prefix}", match[1], '` should be `#{', match[3], "}`"].join
end
if (match = regex_match_group(p, %r{^(/(bin|include|libexec|lib|sbin|share|Frameworks))}i))
problem ["`#", "{prefix}", match[1], '` should be `#{', match[2].downcase, "}`"].join
# match[2] must exist because of the previous line
problem ["`#", "{prefix}", match[1], '` should be `#{', T.must(match[2]).downcase, "}`"].join
end
end
find_every_method_call_by_name(body_node, :depends_on).each do |method|
key, value = destructure_hash(parameters(method).first)
key, value = destructure_hash(parameters(method).fetch(0))
next if key.nil? || value.nil?
next unless (match = regex_match_group(value, /^(lua|perl|python|ruby)(\d*)/))
@@ -785,13 +790,13 @@ module RuboCop
end
find_every_method_call_by_name(body_node, :system).each do |method|
next unless (match = regex_match_group(parameters(method).first, /^(env|export)(\s+)?/))
next unless (match = regex_match_group(parameters(method).fetch(0), /^(env|export)(\s+)?/))
problem "Use `ENV` instead of invoking `#{match[1]}` to modify the environment"
end
find_every_method_call_by_name(body_node, :depends_on).each do |method|
param = parameters(method).first
param = parameters(method).fetch(0)
dep, option_child_nodes = hash_dep(param)
next if dep.nil? || option_child_nodes.empty?
@@ -861,6 +866,8 @@ module RuboCop
end
if find_method_def(processed_source.ast)
raise "unexpected nil value for @offensive_node" unless @offensive_node
problem "Define method `#{method_name(@offensive_node)}` in the class body, not at the top-level"
end
@@ -889,7 +896,7 @@ module RuboCop
find_instance_method_call(body_node, "Dir", :[]) do |method|
next if parameters(method).size != 1
path = parameters(method).first
path = parameters(method).fetch(0)
next unless path.str_type?
next unless (match = regex_match_group(path, /^[^*{},]+$/))
@@ -902,7 +909,7 @@ module RuboCop
.join("|"),
)
find_every_method_call_by_name(body_node, :system).each do |method|
param = parameters(method).first
param = parameters(method).fetch(0)
next unless (match = regex_match_group(param, fileutils_methods))
problem "Use the `#{match}` Ruby method instead of `#{method.source}`"
@@ -954,10 +961,10 @@ module RuboCop
params = parameters(method)
next unless node_equals?(params[0], "make")
params[1..].each do |arg|
params[1..]&.each do |arg|
next unless regex_match_group(arg, /^(checks?|tests?)$/)
offending_node(method)
@offensive_node = method
problem "Formulae in homebrew/core (except e.g. cryptography, libraries) " \
"should not run build-time checks"
end
+9 -6
View File
@@ -16,15 +16,16 @@ module RuboCop
livecheck_node = find_block(formula_nodes.body_node, :livecheck)
return if livecheck_node.blank?
skip = find_every_method_call_by_name(livecheck_node, :skip).first
skip = T.let(find_every_method_call_by_name(livecheck_node, :skip).first,
T.nilable(T.any(RuboCop::AST::Node, String)))
return if skip.blank?
return if find_every_method_call_by_name(livecheck_node).length < 3
offending_node(livecheck_node)
problem "Skipped formulae must not contain other livecheck information." do |corrector|
skip = find_every_method_call_by_name(livecheck_node, :skip).first
skip = find_strings(skip).first
skip = find_every_method_call_by_name(livecheck_node, :skip).fetch(0)
skip = find_strings(skip).fetch(0)
skip = string_content(skip) if skip.present?
corrector.replace(
livecheck_node.source_range,
@@ -74,6 +75,8 @@ module RuboCop
return if skip.present?
livecheck_url_node = find_every_method_call_by_name(livecheck_node, :url).first
return if livecheck_url_node.blank?
livecheck_url = find_strings(livecheck_url_node).first
return if livecheck_url.blank?
@@ -101,7 +104,7 @@ module RuboCop
stable_url = string_content(stable_url) if stable_url.present?
homepage = find_every_method_call_by_name(body_node, :homepage).first
homepage_url = string_content(find_strings(homepage).first) if homepage.present?
homepage_url = string_content(find_strings(homepage).fetch(0)) if homepage.present?
formula_urls = { head: head_url, stable: stable_url, homepage: homepage_url }.compact
@@ -161,13 +164,13 @@ module RuboCop
return if livecheck_regex_node.blank?
regex_node = livecheck_regex_node.descendants.first
pattern = string_content(find_strings(regex_node).first)
pattern = string_content(find_strings(regex_node).fetch(0))
match = pattern.match(TAR_PATTERN)
return if match.blank?
offending_node(regex_node)
problem "Use `\\.t` instead of `#{match}`" do |corrector|
node = find_strings(regex_node).first
node = find_strings(regex_node).fetch(0)
correct = node.source.gsub(TAR_PATTERN, "\\.t")
corrector.replace(node.source_range, correct)
end
+1 -1
View File
@@ -17,7 +17,7 @@ module RuboCop
option_call_nodes = find_every_method_call_by_name(body_node, :option)
option_call_nodes.each do |option_call|
option = parameters(option_call).first
option = parameters(option_call).fetch(0)
offending_node(option_call)
option = string_content(option)
+2 -2
View File
@@ -19,8 +19,8 @@ module RuboCop
external_patches = find_all_blocks(body_node, :patch)
external_patches.each do |patch_block|
url_node = find_every_method_call_by_name(patch_block, :url).first
url_string = parameters(url_node).first
url_node = find_every_method_call_by_name(patch_block, :url).fetch(0)
url_string = parameters(url_node).fetch(0)
sha256_node = find_every_method_call_by_name(patch_block, :sha256).first
sha256_string = parameters(sha256_node).first if sha256_node
patch_problems(url_string, sha256_string)
+24 -3
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module RuboCop
@@ -97,26 +97,38 @@ module RuboCop
private
sig {
params(node: RuboCop::AST::IfNode, receiver: RuboCop::AST::Node, other: T.nilable(RuboCop::AST::Node)).void
}
def register_offense(node, receiver, other)
add_offense(node, message: message(node, receiver, other)) do |corrector|
corrector.replace(node, replacement(receiver, other, node.left_sibling))
end
end
sig { params(node: RuboCop::AST::IfNode).returns(T::Boolean) }
def ignore_if_node?(node)
node.elsif?
end
sig { params(node: T.nilable(RuboCop::AST::Node)).returns(T::Boolean) }
def ignore_other_node?(node)
node && (node.if_type? || node.rescue_type? || node.while_type?)
return false unless node
node.if_type? || node.rescue_type? || node.while_type?
end
sig {
params(node: RuboCop::AST::IfNode, receiver: RuboCop::AST::Node, other: T.nilable(RuboCop::AST::Node))
.returns(String)
}
def message(node, receiver, other)
prefer = replacement(receiver, other, node.left_sibling).gsub(/^\s*|\n/, "")
current = current(node).gsub(/^\s*|\n/, "")
format(MSG, prefer:, current:)
end
sig { params(node: RuboCop::AST::IfNode).returns(String) }
def current(node)
if !node.ternary? && node.source.include?("\n")
"#{node.loc.keyword.with(end_pos: node.condition.loc.selector.end_pos).source} ... end"
@@ -125,8 +137,15 @@ module RuboCop
end
end
sig {
params(
receiver: RuboCop::AST::Node,
other: T.nilable(RuboCop::AST::Node),
left_sibling: T.nilable(T.any(RuboCop::AST::Node, Symbol)),
).returns(String)
}
def replacement(receiver, other, left_sibling)
or_source = if other&.send_type?
or_source = if other.is_a?(RuboCop::AST::SendNode)
build_source_for_or_method(other)
elsif other.nil? || other.nil_type?
""
@@ -138,6 +157,7 @@ module RuboCop
left_sibling ? "(#{replaced})" : replaced
end
sig { params(other: RuboCop::AST::SendNode).returns(String) }
def build_source_for_or_method(other)
if other.parenthesized? || other.method?("[]") || other.arithmetic_operation? || !other.arguments?
" || #{other.source}"
@@ -149,6 +169,7 @@ module RuboCop
end
end
sig { params(node: RuboCop::AST::SendNode).returns(Parser::Source::Range) }
def method_range(node)
range_between(node.source_range.begin_pos, node.first_argument.source_range.begin_pos - 1)
end
@@ -48,7 +48,7 @@ module RuboCop
end
next if required_deps.all? { |dep| uses_from_macos_or_depends_on.include?(dep) }
offending_node(found)
@offensive_node = found
problem "Add `#{kind}` lines above for #{required_deps.map { |req| "`\"#{req}\"`" }.join(" and ")}."
end
end
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "rubocop"
@@ -16,6 +16,7 @@ module RuboCop
# Checks for regex match of pattern in the node and
# sets the appropriate instance variables to report the match.
sig { params(node: RuboCop::AST::Node, pattern: T.any(Regexp, String)).returns(T.nilable(MatchData)) }
def regex_match_group(node, pattern)
string_repr = string_content(node).encode("UTF-8", invalid: :replace)
match_object = string_repr.match(pattern)
@@ -23,15 +24,18 @@ module RuboCop
node_begin_pos = start_column(node)
line_begin_pos = line_start_column(node)
@column = if node_begin_pos == line_begin_pos
node_begin_pos + match_object.begin(0) - line_begin_pos
else
node_begin_pos + match_object.begin(0) - line_begin_pos + 1
end
@length = match_object.to_s.length
@line_no = line_number(node)
@source_buf = source_buffer(node)
@offensive_node = node
@column = T.let(
if node_begin_pos == line_begin_pos
node_begin_pos + match_object.begin(0) - line_begin_pos
else
node_begin_pos + match_object.begin(0) - line_begin_pos + 1
end,
T.nilable(Integer),
)
@length = T.let(match_object.to_s.length, T.nilable(Integer))
@line_no = T.let(line_number(node), T.nilable(Integer))
@source_buf = T.let(source_buffer(node), T.nilable(Parser::Source::Buffer))
@offensive_node = T.let(node, T.nilable(RuboCop::AST::Node))
@offensive_source_range = T.let(
source_range(@source_buf, @line_no, @column, @length),
T.nilable(Parser::Source::Range),
@@ -40,11 +44,13 @@ module RuboCop
end
# Returns the begin position of the node's line in source code.
sig { params(node: RuboCop::AST::Node).returns(Integer) }
def line_start_column(node)
node.source_range.source_buffer.line_range(node.loc.line).begin_pos
end
# Returns the begin position of the node in source code.
sig { params(node: RuboCop::AST::Node).returns(Integer) }
def start_column(node)
node.source_range.begin_pos
end
@@ -62,6 +68,7 @@ module RuboCop
end
# Returns the string representation if node is of type str(plain) or dstr(interpolated) or const.
sig { params(node: RuboCop::AST::Node, strip_dynamic: T::Boolean).returns(String) }
def string_content(node, strip_dynamic: false)
case node.type
when :str
@@ -77,9 +84,10 @@ module RuboCop
end
content
when :send
if node.method?(:+) && (node.receiver.str_type? || node.receiver.dstr_type?)
send_node = T.cast(node, RuboCop::AST::SendNode)
if send_node.method?(:+) && (send_node.receiver.str_type? || send_node.receiver.dstr_type?)
content = string_content(node.receiver)
arg = node.arguments.first
arg = send_node.arguments.first
content += string_content(arg) if arg
content
else
@@ -94,19 +102,22 @@ module RuboCop
end
end
sig { params(msg: String, block: T.nilable(T.proc.params(corrector: RuboCop::Cop::Corrector).void)).void }
def problem(msg, &block)
add_offense(@offensive_node, message: msg, &block)
end
# Returns all string nodes among the descendants of given node.
sig { params(node: T.nilable(RuboCop::AST::Node)).returns(T::Array[RuboCop::AST::Node]) }
def find_strings(node)
return [] if node.nil?
return [node] if node.str_type?
node.each_descendant(:str)
node.each_descendant(:str).to_a
end
# Returns method_node matching method_name.
sig { params(node: RuboCop::AST::Node, method_name: Symbol).returns(T.nilable(RuboCop::AST::Node)) }
def find_node_method_by_name(node, method_name)
return if node.nil?
@@ -122,6 +133,7 @@ module RuboCop
end
# Gets/sets the given node as the offending node when required in custom cops.
sig { params(node: T.nilable(RuboCop::AST::Node)).returns(T.nilable(RuboCop::AST::Node)) }
def offending_node(node = nil)
return @offensive_node if node.nil?
@@ -129,21 +141,28 @@ module RuboCop
end
# Returns an array of method call nodes matching method_name inside node with depth first order (child nodes).
sig {
params(node: T.nilable(RuboCop::AST::Node), method_name: Symbol).returns(T::Array[RuboCop::AST::SendNode])
}
def find_method_calls_by_name(node, method_name)
return if node.nil?
return [] if node.nil?
nodes = node.each_child_node(:send).select { |method_node| method_name == method_node.method_name }
# The top level node can be a method
nodes << node if node.send_type? && node.method_name == method_name
nodes << node if node.is_a?(RuboCop::AST::SendNode) && node.method_name == method_name
nodes
end
# Returns an array of method call nodes matching method_name in every descendant of node.
# Returns every method call if no method_name is passed.
sig {
params(node: T.nilable(RuboCop::AST::Node), method_name: T.nilable(Symbol))
.returns(T::Array[RuboCop::AST::SendNode])
}
def find_every_method_call_by_name(node, method_name = nil)
return if node.nil?
return [] if node.nil?
node.each_descendant(:send).select do |method_node|
method_name.nil? ||
@@ -156,8 +175,12 @@ module RuboCop
# - matches function call: `foo(*args, **kwargs)`
# - does not match method calls: `foo.bar(*args, **kwargs)`
# - returns every function call if no func_name is passed
sig {
params(node: T.nilable(RuboCop::AST::Node), func_name: T.nilable(Symbol))
.returns(T::Array[T.any(RuboCop::AST::BlockNode, RuboCop::AST::SendNode)])
}
def find_every_func_call_by_name(node, func_name = nil)
return if node.nil?
return [] if node.nil?
node.each_descendant(:send).select do |func_node|
func_node.receiver.nil? && (func_name.nil? || func_name == func_node.method_name)
@@ -166,11 +189,19 @@ module RuboCop
# Given a method_name and arguments, yields to a block with
# matching method passed as a parameter to the block.
def find_method_with_args(node, method_name, *args)
sig {
params(
node: T.nilable(RuboCop::AST::Node),
method_name: Symbol,
args: Object,
_block: T.nilable(T.proc.params(method: RuboCop::AST::Node).void),
).returns(T::Array[RuboCop::AST::SendNode])
}
def find_method_with_args(node, method_name, *args, &_block)
methods = find_every_method_call_by_name(node, method_name)
methods.each do |method|
next unless parameters_passed?(method, args)
return true unless block_given?
return [] unless block_given?
yield method
end
@@ -191,7 +222,15 @@ module RuboCop
# ```ruby
# find_instance_method_call(node, :build, :head?)
# ```
def find_instance_method_call(node, instance, method_name)
sig {
params(
node: T.nilable(RuboCop::AST::Node),
instance: T.any(String, Symbol),
method_name: T.nilable(Symbol),
_block: T.nilable(T.proc.params(method: RuboCop::AST::SendNode).void),
).returns(T.anything)
}
def find_instance_method_call(node, instance, method_name, &_block)
methods = find_every_method_call_by_name(node, method_name)
methods.each do |method|
next if method.receiver.nil?
@@ -214,7 +253,14 @@ module RuboCop
# ```ruby
# find_instance_call(node, "ARGV")
# ```
def find_instance_call(node, name)
sig {
params(
node: RuboCop::AST::Node,
name: String,
_block: T.nilable(T.proc.params(method: RuboCop::AST::Node).void),
).returns(T.anything)
}
def find_instance_call(node, name, &_block)
node.each_descendant(:send) do |method_node|
next if method_node.receiver.nil?
next if method_node.receiver.const_name != name &&
@@ -229,7 +275,14 @@ module RuboCop
# Find CONSTANTs in the source.
# If block given, yield matching nodes.
def find_const(node, const_name)
sig {
params(
node: T.nilable(RuboCop::AST::Node),
const_name: String,
_block: T.nilable(T.proc.params(const: RuboCop::AST::Node).void),
).returns(T.anything)
}
def find_const(node, const_name, &_block)
return if node.nil?
node.each_descendant(:const) do |const_node|
@@ -243,11 +296,15 @@ module RuboCop
end
# To compare node with appropriate Ruby variable.
sig { params(node: T.nilable(RuboCop::AST::Node), var: Object).returns(T::Boolean) }
def node_equals?(node, var)
node == Parser::CurrentRuby.parse(var.inspect)
end
# Returns a block named block_name inside node.
sig {
params(node: T.nilable(RuboCop::AST::Node), block_name: Symbol).returns(T.nilable(RuboCop::AST::BlockNode))
}
def find_block(node, block_name)
return if node.nil?
@@ -263,16 +320,26 @@ module RuboCop
end
# Returns an array of block nodes named block_name inside node.
sig {
params(node: T.nilable(RuboCop::AST::Node), block_name: Symbol).returns(T::Array[RuboCop::AST::BlockNode])
}
def find_blocks(node, block_name)
return if node.nil?
return [] if node.nil?
node.each_child_node(:block).select { |block_node| block_name == block_node.method_name }
end
# Returns an array of block nodes of any depth below node in AST.
# If a block is given then yields matching block node to the block!
def find_all_blocks(node, block_name)
return if node.nil?
sig {
params(
node: T.nilable(RuboCop::AST::Node),
block_name: Symbol,
_block: T.nilable(T.proc.params(block: RuboCop::AST::BlockNode).void),
).returns(T::Array[RuboCop::AST::BlockNode])
}
def find_all_blocks(node, block_name, &_block)
return [] if node.nil?
blocks = node.each_descendant(:block).select { |block_node| block_name == block_node.method_name }
return blocks unless block_given?
@@ -285,6 +352,10 @@ module RuboCop
# Returns a method definition node with method_name.
# Returns first method def if method_name is nil.
sig {
params(node: T.nilable(RuboCop::AST::Node), method_name: T.nilable(Symbol))
.returns(T.nilable(RuboCop::AST::Node))
}
def find_method_def(node, method_name = nil)
return if node.nil?
@@ -303,6 +374,7 @@ module RuboCop
end
# Check if a block method is called inside a block.
sig { params(node: RuboCop::AST::BlockNode, method_name: Symbol).returns(T::Boolean) }
def block_method_called_in_block?(node, method_name)
node.body.each_child_node do |call_node|
next if !call_node.block_type? && !call_node.send_type?
@@ -316,8 +388,9 @@ module RuboCop
# Check if method_name is called among the direct children nodes in the given node.
# Check if the node itself is the method.
sig { params(node: RuboCop::AST::Node, method_name: Symbol).returns(T::Boolean) }
def method_called?(node, method_name)
if node.send_type? && node.method_name == method_name
if node.is_a?(RuboCop::AST::SendNode) && node.method_name == method_name
offending_node(node)
return true
end
@@ -331,6 +404,7 @@ module RuboCop
end
# Check if method_name is called among every descendant node of given node.
sig { params(node: RuboCop::AST::Node, method_name: Symbol).returns(T::Boolean) }
def method_called_ever?(node, method_name)
node.each_descendant(:send) do |call_node|
next if call_node.method_name != method_name
@@ -342,6 +416,10 @@ module RuboCop
end
# Checks for precedence; returns the first pair of precedence-violating nodes.
sig {
params(first_nodes: T::Array[RuboCop::AST::Node], next_nodes: T::Array[RuboCop::AST::Node])
.returns(T.nilable([RuboCop::AST::Node, RuboCop::AST::Node]))
}
def check_precedence(first_nodes, next_nodes)
next_nodes.each do |each_next_node|
first_nodes.each do |each_first_node|
@@ -352,6 +430,7 @@ module RuboCop
end
# If first node does not precede next_node, sets appropriate instance variables for reporting.
sig { params(first_node: RuboCop::AST::Node, next_node: RuboCop::AST::Node).returns(T::Boolean) }
def component_precedes?(first_node, next_node)
return false if line_number(first_node) < line_number(next_node)
@@ -360,21 +439,28 @@ module RuboCop
end
# Check if negation is present in the given node.
sig { params(node: RuboCop::AST::Node).returns(T::Boolean) }
def expression_negated?(node)
return false unless node.parent&.send_type?
return false unless node.parent.method_name.equal?(:!)
offending_node(node.parent)
!!offending_node(node.parent)
end
# Returns the array of arguments of the method_node.
sig { params(method_node: RuboCop::AST::Node).returns(T::Array[RuboCop::AST::Node]) }
def parameters(method_node)
method_node.arguments if method_node.send_type? || method_node.block_type?
if method_node.is_a?(RuboCop::AST::SendNode) || method_node.is_a?(RuboCop::AST::BlockNode)
method_node.arguments
else
[]
end
end
# Returns true if the given parameters are present in method call
# and sets the method call as the offending node.
# Params can be string, symbol, array, hash, matching regex.
sig { params(method_node: RuboCop::AST::Node, params: T::Array[Object]).returns(T::Boolean) }
def parameters_passed?(method_node, params)
method_params = parameters(method_node)
@offensive_node = method_node
@@ -390,36 +476,32 @@ module RuboCop
end
# Returns the ending position of the node in source code.
sig { params(node: RuboCop::AST::Node).returns(Integer) }
def end_column(node)
node.source_range.end_pos
end
# Returns the class node's name, or nil if not a class node.
sig { params(node: RuboCop::AST::Node).returns(T.nilable(String)) }
def class_name(node)
@offensive_node = node
node.const_name
end
# Returns the method name for a def node.
sig { params(node: RuboCop::AST::Node).returns(T.nilable(Symbol)) }
def method_name(node)
node.children[0] if node.def_type?
end
# Returns the node size in the source code.
def size(node)
node.source_range.size
end
# Returns the block length of the block node.
def block_size(block)
block.loc.end.line - block.loc.begin.line
end
# Returns printable component name.
sig { params(component_node: RuboCop::AST::Node).returns(T.nilable(Symbol)) }
def format_component(component_node)
return component_node.method_name if component_node.send_type? || component_node.block_type?
method_name(component_node) if component_node.def_type?
if component_node.is_a?(RuboCop::AST::SendNode) || component_node.is_a?(RuboCop::AST::BlockNode)
component_node.method_name
elsif component_node.def_type?
method_name(component_node)
end
end
end
end
@@ -68,16 +68,16 @@ module RuboCop
end
end
offending_node(on_system_node)
@offensive_node = on_system_node
problem "Instead of using `#{on_system_node.source}` in `#{parent_string}`, " \
"use `#{if_statement_string}#{if_conditional}`." do |corrector|
block_node = offending_node.parent
block_node = @offensive_node.parent
next if block_node.type != :block
# TODO: could fix corrector to handle this but punting for now.
next if block_node.single_line?
source_range = offending_node.source_range.join(offending_node.parent.loc.begin)
source_range = @offensive_node.source_range.join(@offensive_node.parent.loc.begin)
corrector.replace(source_range, "#{if_statement_string}#{if_conditional}")
end
end
+21 -6
View File
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "rubocops/shared/helper_functions"
@@ -13,10 +13,17 @@ module RuboCop
#
# @param urls [Array] url/mirror method call nodes
# @param regex [Regexp] pattern to match URLs
def audit_urls(urls, regex)
sig {
params(
urls: T::Array[T.any(RuboCop::AST::BlockNode, RuboCop::AST::SendNode)],
regex: T.any(Regexp, String),
_block: T.proc.params(match_object: MatchData, url: String, index: Integer).void,
).void
}
def audit_urls(urls, regex, &_block)
urls.each_with_index do |url_node, index|
if @type == :cask
url_string_node = url_node.first_argument
url_string_node = T.cast(url_node, RuboCop::AST::SendNode).first_argument
url_string = url_node.source
else
url_string_node = parameters(url_node).first
@@ -34,8 +41,16 @@ module RuboCop
end
end
sig {
params(
type: Symbol,
urls: T::Array[T.any(RuboCop::AST::BlockNode, RuboCop::AST::SendNode)],
mirrors: T::Array[T.any(RuboCop::AST::BlockNode, RuboCop::AST::SendNode)],
livecheck_urls: T::Array[String],
).void
}
def audit_url(type, urls, mirrors, livecheck_urls: [])
@type = type
@type = T.let(type, T.nilable(Symbol))
# URLs must be ASCII; IDNs must be punycode
ascii_pattern = /[^\p{ASCII}]+/
@@ -69,7 +84,7 @@ module RuboCop
next if livecheck_urls.include?(url)
fixed = "https://www.apache.org/dyn/closer.lua?path=#{match[1]}"
url_parameter_node = parameters(urls.fetch(index)).first
url_parameter_node = parameters(urls.fetch(index)).fetch(0)
problem "#{url} should be: #{fixed}" do |corrector|
corrector.replace(url_parameter_node.source_range, "\"#{fixed}\"")
end
@@ -87,7 +102,7 @@ module RuboCop
audit_urls(mirrors, /.*/) do |_, mirror|
urls.each do |url|
url_string = string_content(parameters(url).first)
url_string = string_content(parameters(url).fetch(0))
next unless url_string.eql?(mirror)
problem "URL should not be duplicated as a mirror: #{url_string}"
+4 -4
View File
@@ -129,8 +129,8 @@ module RuboCop
return if formula_tap != "homebrew-core"
find_method_calls_by_name(body_node, :url).each do |url|
next unless string_content(parameters(url).first).match?(/\.git$/)
next if url_has_revision?(parameters(url).last)
next unless string_content(parameters(url).fetch(0)).match?(/\.git$/)
next if url_has_revision?(parameters(url).fetch(-1))
offending_node(url)
problem "Formulae in homebrew/core should specify a revision for Git URLs"
@@ -152,8 +152,8 @@ module RuboCop
return if formula_tap != "homebrew-core"
find_method_calls_by_name(body_node, :url).each do |url|
next unless string_content(parameters(url).first).match?(/\.git$/)
next if url_has_tag?(parameters(url).last)
next unless string_content(parameters(url).fetch(0)).match?(/\.git$/)
next if url_has_tag?(parameters(url).fetch(-1))
offending_node(url)
problem "Formulae in homebrew/core should specify a tag for Git URLs"
+5 -4
View File
@@ -106,10 +106,11 @@ module RuboCop
@offensive_node = method
problem "`uses_from_macos` should not be used when Linux is required." if depends_on_linux
dep = if parameters(method).first.instance_of?(RuboCop::AST::StrNode)
parameters(method).first
elsif parameters(method).first.instance_of?(RuboCop::AST::HashNode)
parameters(method).first.keys.first
first_argument = parameters(method).first
dep = if first_argument.instance_of?(RuboCop::AST::StrNode)
first_argument
elsif first_argument.instance_of?(RuboCop::AST::HashNode)
first_argument.keys.first
end
dep_name = string_content(dep)
+1 -1
View File
@@ -13,7 +13,7 @@ module RuboCop
version_node = find_node_method_by_name(formula_nodes.body_node, :version)
return unless version_node
version = string_content(parameters(version_node).first)
version = string_content(parameters(version_node).fetch(0))
problem "Version is set to an empty string" if version.empty?
+1 -11
View File
@@ -11,16 +11,6 @@ class Integer
)
.returns(Integer)
}
sig { params(other: T.anything).returns(NilClass) }
sig { params(other: T.anything).returns(T.nilable(Integer)) }
def <=>(other); end
end
# https://github.com/sorbet/sorbet/pull/9847
class IO
# Waits until IO is readable and returns a truthy value, or a falsy value when
# times out. Returns a truthy value immediately when buffered data is available.
#
# You must require 'io/wait' to use this method.
sig { params(timeout: T.nilable(T.any(Float, Integer, Rational))).returns(T.nilable(T.any(IO, T::Boolean))) }
def wait_readable(timeout = nil); end
end