From d03d51d887e1748beb430e31c129d5594ed84f41 Mon Sep 17 00:00:00 2001 From: Douglas Eichelberger Date: Fri, 6 Feb 2026 04:27:17 -0800 Subject: [PATCH] Enable strict typing in Library/Homebrew/rubocops/ --- .vscode/settings.json | 3 +- Library/Homebrew/rubocops/bottle.rb | 2 +- .../rubocops/cask/array_alphabetization.rb | 18 +- .../Homebrew/rubocops/cask/ast/cask_header.rb | 19 +- Library/Homebrew/rubocops/cask/ast/stanza.rb | 4 +- .../Homebrew/rubocops/cask/extend/node.rbi | 5 + .../Homebrew/rubocops/cask/stanza_grouping.rb | 36 +++- .../Homebrew/rubocops/cask/stanza_order.rb | 13 +- Library/Homebrew/rubocops/cask/url.rb | 2 +- Library/Homebrew/rubocops/components_order.rb | 41 ++++- Library/Homebrew/rubocops/conflicts.rb | 6 +- Library/Homebrew/rubocops/dependency_order.rb | 25 ++- .../Homebrew/rubocops/deprecate_disable.rb | 8 +- .../Homebrew/rubocops/extend/formula_cop.rb | 19 +- Library/Homebrew/rubocops/homepage.rb | 2 +- Library/Homebrew/rubocops/keg_only.rb | 16 +- Library/Homebrew/rubocops/lines.rb | 107 ++++++------ Library/Homebrew/rubocops/livecheck.rb | 15 +- Library/Homebrew/rubocops/options.rb | 2 +- Library/Homebrew/rubocops/patches.rb | 4 +- Library/Homebrew/rubocops/presence.rb | 27 ++- .../resource_requires_dependencies.rb | 2 +- .../rubocops/shared/helper_functions.rb | 164 +++++++++++++----- .../shared/on_system_conditionals_helper.rb | 6 +- .../Homebrew/rubocops/shared/url_helper.rb | 27 ++- Library/Homebrew/rubocops/urls.rb | 8 +- Library/Homebrew/rubocops/uses_from_macos.rb | 9 +- Library/Homebrew/rubocops/version.rb | 2 +- Library/Homebrew/sorbet/rbi/upstream.rbi | 12 +- 29 files changed, 404 insertions(+), 200 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 14ab39c7be..14864f0d4a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" } diff --git a/Library/Homebrew/rubocops/bottle.rb b/Library/Homebrew/rubocops/bottle.rb index 866decd520..f1ba94b168 100644 --- a/Library/Homebrew/rubocops/bottle.rb +++ b/Library/Homebrew/rubocops/bottle.rb @@ -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) diff --git a/Library/Homebrew/rubocops/cask/array_alphabetization.rb b/Library/Homebrew/rubocops/cask/array_alphabetization.rb index 36c3cbf11b..76ab3eb475 100644 --- a/Library/Homebrew/rubocops/cask/array_alphabetization.rb +++ b/Library/Homebrew/rubocops/cask/array_alphabetization.rb @@ -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 diff --git a/Library/Homebrew/rubocops/cask/ast/cask_header.rb b/Library/Homebrew/rubocops/cask/ast/cask_header.rb index 2b4ed0ef1e..3501e1d4b1 100644 --- a/Library/Homebrew/rubocops/cask/ast/cask_header.rb +++ b/Library/Homebrew/rubocops/cask/ast/cask_header.rb @@ -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 diff --git a/Library/Homebrew/rubocops/cask/ast/stanza.rb b/Library/Homebrew/rubocops/cask/ast/stanza.rb index c518dc9ae1..2e2ca43b1b 100644 --- a/Library/Homebrew/rubocops/cask/ast/stanza.rb +++ b/Library/Homebrew/rubocops/cask/ast/stanza.rb @@ -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 diff --git a/Library/Homebrew/rubocops/cask/extend/node.rbi b/Library/Homebrew/rubocops/cask/extend/node.rbi index d5850b29bd..533fcf5d8c 100644 --- a/Library/Homebrew/rubocops/cask/extend/node.rbi +++ b/Library/Homebrew/rubocops/cask/extend/node.rbi @@ -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 diff --git a/Library/Homebrew/rubocops/cask/stanza_grouping.rb b/Library/Homebrew/rubocops/cask/stanza_grouping.rb index 09190f0baa..c13815cd4a 100644 --- a/Library/Homebrew/rubocops/cask/stanza_grouping.rb +++ b/Library/Homebrew/rubocops/cask/stanza_grouping.rb @@ -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 diff --git a/Library/Homebrew/rubocops/cask/stanza_order.rb b/Library/Homebrew/rubocops/cask/stanza_order.rb index 8de4858936..c6422aef24 100644 --- a/Library/Homebrew/rubocops/cask/stanza_order.rb +++ b/Library/Homebrew/rubocops/cask/stanza_order.rb @@ -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 = "`%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 diff --git a/Library/Homebrew/rubocops/cask/url.rb b/Library/Homebrew/rubocops/cask/url.rb index 2cfe6bf69f..bcc04603c5 100644 --- a/Library/Homebrew/rubocops/cask/url.rb +++ b/Library/Homebrew/rubocops/cask/url.rb @@ -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 diff --git a/Library/Homebrew/rubocops/components_order.rb b/Library/Homebrew/rubocops/components_order.rb index 4f20e38c49..532139ecf7 100644 --- a/Library/Homebrew/rubocops/components_order.rb +++ b/Library/Homebrew/rubocops/components_order.rb @@ -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 diff --git a/Library/Homebrew/rubocops/conflicts.rb b/Library/Homebrew/rubocops/conflicts.rb index f7ee2f9124..5b913811c1 100644 --- a/Library/Homebrew/rubocops/conflicts.rb +++ b/Library/Homebrew/rubocops/conflicts.rb @@ -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 diff --git a/Library/Homebrew/rubocops/dependency_order.rb b/Library/Homebrew/rubocops/dependency_order.rb index 4ebf354573..15e67f3ceb 100644 --- a/Library/Homebrew/rubocops/dependency_order.rb +++ b/Library/Homebrew/rubocops/dependency_order.rb @@ -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 diff --git a/Library/Homebrew/rubocops/deprecate_disable.rb b/Library/Homebrew/rubocops/deprecate_disable.rb index 4078f910a9..7379804b5c 100644 --- a/Library/Homebrew/rubocops/deprecate_disable.rb +++ b/Library/Homebrew/rubocops/deprecate_disable.rb @@ -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 diff --git a/Library/Homebrew/rubocops/extend/formula_cop.rb b/Library/Homebrew/rubocops/extend/formula_cop.rb index 7b1742fb88..e186f58b16 100644 --- a/Library/Homebrew/rubocops/extend/formula_cop.rb +++ b/Library/Homebrew/rubocops/extend/formula_cop.rb @@ -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 diff --git a/Library/Homebrew/rubocops/homepage.rb b/Library/Homebrew/rubocops/homepage.rb index 618fc800f8..77da612680 100644 --- a/Library/Homebrew/rubocops/homepage.rb +++ b/Library/Homebrew/rubocops/homepage.rb @@ -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) diff --git a/Library/Homebrew/rubocops/keg_only.rb b/Library/Homebrew/rubocops/keg_only.rb index 309d8f1d66..5053492f40 100644 --- a/Library/Homebrew/rubocops/keg_only.rb +++ b/Library/Homebrew/rubocops/keg_only.rb @@ -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 diff --git a/Library/Homebrew/rubocops/lines.rb b/Library/Homebrew/rubocops/lines.rb index 6c483f5cc9..088e6f5f0e 100644 --- a/Library/Homebrew/rubocops/lines.rb +++ b/Library/Homebrew/rubocops/lines.rb @@ -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 ` 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 ` 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 diff --git a/Library/Homebrew/rubocops/livecheck.rb b/Library/Homebrew/rubocops/livecheck.rb index bc0740c400..285fb92b52 100644 --- a/Library/Homebrew/rubocops/livecheck.rb +++ b/Library/Homebrew/rubocops/livecheck.rb @@ -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 diff --git a/Library/Homebrew/rubocops/options.rb b/Library/Homebrew/rubocops/options.rb index 3b0e479dc4..b4f8c96aa6 100644 --- a/Library/Homebrew/rubocops/options.rb +++ b/Library/Homebrew/rubocops/options.rb @@ -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) diff --git a/Library/Homebrew/rubocops/patches.rb b/Library/Homebrew/rubocops/patches.rb index 1a8859d452..ef6f670e77 100644 --- a/Library/Homebrew/rubocops/patches.rb +++ b/Library/Homebrew/rubocops/patches.rb @@ -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) diff --git a/Library/Homebrew/rubocops/presence.rb b/Library/Homebrew/rubocops/presence.rb index e710713d13..f8d10911ec 100644 --- a/Library/Homebrew/rubocops/presence.rb +++ b/Library/Homebrew/rubocops/presence.rb @@ -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 diff --git a/Library/Homebrew/rubocops/resource_requires_dependencies.rb b/Library/Homebrew/rubocops/resource_requires_dependencies.rb index 978f0356d1..d7ef2c2d61 100644 --- a/Library/Homebrew/rubocops/resource_requires_dependencies.rb +++ b/Library/Homebrew/rubocops/resource_requires_dependencies.rb @@ -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 diff --git a/Library/Homebrew/rubocops/shared/helper_functions.rb b/Library/Homebrew/rubocops/shared/helper_functions.rb index 42147e6f67..d7cbdb5b7c 100644 --- a/Library/Homebrew/rubocops/shared/helper_functions.rb +++ b/Library/Homebrew/rubocops/shared/helper_functions.rb @@ -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 diff --git a/Library/Homebrew/rubocops/shared/on_system_conditionals_helper.rb b/Library/Homebrew/rubocops/shared/on_system_conditionals_helper.rb index beb8a48a05..29dca6edf4 100644 --- a/Library/Homebrew/rubocops/shared/on_system_conditionals_helper.rb +++ b/Library/Homebrew/rubocops/shared/on_system_conditionals_helper.rb @@ -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 diff --git a/Library/Homebrew/rubocops/shared/url_helper.rb b/Library/Homebrew/rubocops/shared/url_helper.rb index 61e6df162d..a33b7af1f3 100644 --- a/Library/Homebrew/rubocops/shared/url_helper.rb +++ b/Library/Homebrew/rubocops/shared/url_helper.rb @@ -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}" diff --git a/Library/Homebrew/rubocops/urls.rb b/Library/Homebrew/rubocops/urls.rb index 027a9861f4..c3ff5c8e09 100644 --- a/Library/Homebrew/rubocops/urls.rb +++ b/Library/Homebrew/rubocops/urls.rb @@ -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" diff --git a/Library/Homebrew/rubocops/uses_from_macos.rb b/Library/Homebrew/rubocops/uses_from_macos.rb index 8ebbc96483..cfe249b449 100644 --- a/Library/Homebrew/rubocops/uses_from_macos.rb +++ b/Library/Homebrew/rubocops/uses_from_macos.rb @@ -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) diff --git a/Library/Homebrew/rubocops/version.rb b/Library/Homebrew/rubocops/version.rb index 4492e57d9e..7b40ac7785 100644 --- a/Library/Homebrew/rubocops/version.rb +++ b/Library/Homebrew/rubocops/version.rb @@ -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? diff --git a/Library/Homebrew/sorbet/rbi/upstream.rbi b/Library/Homebrew/sorbet/rbi/upstream.rbi index 3ea4c2ebfb..e79579b653 100644 --- a/Library/Homebrew/sorbet/rbi/upstream.rbi +++ b/Library/Homebrew/sorbet/rbi/upstream.rbi @@ -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