From 924bcef105c6d6550452895c6d822d20542bbb59 Mon Sep 17 00:00:00 2001 From: Sam Bostock Date: Wed, 24 Jan 2024 23:36:48 -0500 Subject: [PATCH] [WIP] Handle all modules extending NodePattern::Macros --- lib/tapioca/dsl/compilers/rubocop.rb | 15 ++-- lib/tapioca/dsl/extensions/rubocop.rb | 2 +- lib/tapioca/runtime/reflection.rb | 30 ++++---- spec/tapioca/dsl/compilers/rubocop_spec.rb | 82 +++++++++++++--------- 4 files changed, 75 insertions(+), 54 deletions(-) diff --git a/lib/tapioca/dsl/compilers/rubocop.rb b/lib/tapioca/dsl/compilers/rubocop.rb index 200916503..dcfdcb62c 100644 --- a/lib/tapioca/dsl/compilers/rubocop.rb +++ b/lib/tapioca/dsl/compilers/rubocop.rb @@ -1,11 +1,7 @@ # typed: strict # frozen_string_literal: true -begin - require "rubocop" -rescue LoadError - return -end +return unless defined?(RuboCop::AST::NodePattern::Macros) module Tapioca module Dsl @@ -34,14 +30,17 @@ module Compilers # `without_defaults_*` methods class RuboCop < Compiler ConstantType = type_member do - { fixed: T.all(T.class_of(::RuboCop::AST::NodePattern::Macros), Extensions::RuboCop) } + { fixed: T.all(Module, Extensions::RuboCop) } end class << self extend T::Sig - sig { override.returns(T::Enumerable[T.class_of(::RuboCop::AST::NodePattern::Macros)]) } + sig { override.returns(T::Array[T.all(Module, Extensions::RuboCop)]) } def gather_constants - descendants_of(::RuboCop::AST::NodePattern::Macros).select { |constant| name_of(constant) } + T.cast( + extenders_of(::RuboCop::AST::NodePattern::Macros).select { |constant| name_of(constant) }, + T::Array[T.all(Module, Extensions::RuboCop)], + ) end end diff --git a/lib/tapioca/dsl/extensions/rubocop.rb b/lib/tapioca/dsl/extensions/rubocop.rb index 24eaa74aa..f8e052456 100644 --- a/lib/tapioca/dsl/extensions/rubocop.rb +++ b/lib/tapioca/dsl/extensions/rubocop.rb @@ -1,7 +1,7 @@ # typed: strict # frozen_string_literal: true -return unless defined?(RuboCop) +return unless defined?(RuboCop::AST::NodePattern::Macros) module Tapioca module Dsl diff --git a/lib/tapioca/runtime/reflection.rb b/lib/tapioca/runtime/reflection.rb index 3996016ca..378b5d1c2 100644 --- a/lib/tapioca/runtime/reflection.rb +++ b/lib/tapioca/runtime/reflection.rb @@ -146,7 +146,7 @@ def method_of(constant, method) METHOD_METHOD.bind_call(constant, method) end - # Returns an array with all modules that are < than the supplied module. + # Returns an array with all classes that are < than the supplied class. # # class C; end # descendants_of(C) # => [] @@ -159,26 +159,30 @@ def method_of(constant, method) # # class D < C; end # descendants_of(C) # => [B, A, D] - # - # module M; end - # class E - # include M - # end - # descendants_of(M) # => [E] sig do type_parameters(:U) - .params(mod: T.all(Module, T.type_parameter(:U))) + .params(klass: T.all(T::Class[T.anything], T.type_parameter(:U))) .returns(T::Array[T.type_parameter(:U)]) end - def descendants_of(mod) - result = ObjectSpace - .each_object(Module) - .select { |m| T.cast(m, Module) < mod } - .reject { |m| T.cast(m, Module).singleton_class? || T.unsafe(m) == mod } + def descendants_of(klass) + result = ObjectSpace.each_object(klass.singleton_class).reject do |k| + T.cast(k, Module).singleton_class? || T.unsafe(k) == klass + end T.unsafe(result) end + # Returns an array with all modules which extend the supplied module + # (i.e. all modules whose singleton class, or ancestor thereof, includes the supplied module). + sig { params(mod: Module).returns(T::Array[Module]) } + def extenders_of(mod) + result = ObjectSpace.each_object(Module).select do |m| + T.cast(m, Module).singleton_class.included_modules.include?(mod) + end + + T.cast(result, T::Array[Module]) + end + # Examines the call stack to identify the closest location where a "require" is performed # by searching for the label "". If none is found, it returns the location # labeled "
", which is the original call site. diff --git a/spec/tapioca/dsl/compilers/rubocop_spec.rb b/spec/tapioca/dsl/compilers/rubocop_spec.rb index 84fb64571..021e6418f 100644 --- a/spec/tapioca/dsl/compilers/rubocop_spec.rb +++ b/spec/tapioca/dsl/compilers/rubocop_spec.rb @@ -2,20 +2,20 @@ # frozen_string_literal: true require "spec_helper" -require "rubocop" -require "rubocop-sorbet" +# require "rubocop" +# require "rubocop-sorbet" module Tapioca module Dsl module Compilers class RuboCopSpec < ::DslSpec - # Collect constants from gems, before defining any in tests. - EXISTING_CONSTANTS = T.let( - Runtime::Reflection - .descendants_of(::RuboCop::Cop::Base) - .filter_map { |constant| Runtime::Reflection.name_of(constant) }, - T::Array[String], - ) + # # Collect constants from gems, before defining any in tests. + # EXISTING_CONSTANTS = T.let( + # Runtime::Reflection + # .extenders_of(::RuboCop::AST::NodePattern::Macros) + # .filter_map { |constant| Runtime::Reflection.name_of(constant) }, + # T::Array[String], + # ) class << self extend T::Sig @@ -30,20 +30,24 @@ def target_class_file describe "Tapioca::Dsl::Compilers::RuboCop" do sig { void } def before_setup + require "rubocop" + require "rubocop-sorbet" require "tapioca/dsl/extensions/rubocop" super end describe "initialize" do it "gathered constants exclude irrelevant classes" do - add_ruby_file("content.rb", <<~RUBY) - class Unrelated - end - RUBY - assert_empty(relevant_gathered_constants) + gathered_constants = gather_constants do + add_ruby_file("content.rb", <<~RUBY) + class Unrelated + end + RUBY + end + assert_empty(gathered_constants) end - it "gathers constants inheriting RuboCop::Cop::Base in gems" do + it "gathers constants extending RuboCop::AST::NodePattern::Macros in gems" do # Sample of miscellaneous constants that should be found from Rubocop and plugins missing_constants = [ "RuboCop::Cop::Bundler::GemVersion", @@ -61,27 +65,33 @@ class Unrelated assert_empty(missing_constants, "expected constants to be gathered") end - it "gathers constants inheriting from RuboCop::Cop::Base in the host app" do - add_ruby_file("content.rb", <<~RUBY) - class MyCop < ::RuboCop::Cop::Base - end + it "gathers constants extending RuboCop::AST::NodePattern::Macros in the host app" do + gathered_constants = gather_constants do + add_ruby_file("content.rb", <<~RUBY) + class MyCop < ::RuboCop::Cop::Base + end - class MyLegacyCop < ::RuboCop::Cop::Cop - end + class MyLegacyCop < ::RuboCop::Cop::Cop + end - module ::RuboCop - module Cop - module MyApp - class MyNamespacedCop < Base + module MyMacroModule + extend ::RuboCop::AST::NodePattern::Macros + end + + module ::RuboCop + module Cop + module MyApp + class MyNamespacedCop < Base + end end end end - end - RUBY + RUBY + end assert_equal( - ["MyCop", "MyLegacyCop", "RuboCop::Cop::MyApp::MyNamespacedCop"], - relevant_gathered_constants, + ["MyCop", "MyLegacyCop", "MyMacroModule", "RuboCop::Cop::MyApp::MyNamespacedCop"], + gathered_constants, ) end end @@ -155,9 +165,17 @@ def without_defaults_some_search_with_params_and_defaults(param0, param1, two:); private - sig { returns(T::Array[String]) } - def relevant_gathered_constants - gathered_constants - EXISTING_CONSTANTS + # Gathers constants introduced in the given block excluding constants that already existed prior to the block. + sig { params(block: T.proc.void).returns(T::Array[String]) } + def gather_constants(&block) + existing_constants = T.let( + Runtime::Reflection + .extenders_of(::RuboCop::AST::NodePattern::Macros) + .filter_map { |constant| Runtime::Reflection.name_of(constant) }, + T::Array[String], + ) + yield + gathered_constants - existing_constants end end end