Skip to content

Commit

Permalink
[WIP] Handle all modules extending NodePattern::Macros
Browse files Browse the repository at this point in the history
  • Loading branch information
sambostock committed Jan 25, 2024
1 parent e6b12e1 commit 924bcef
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 54 deletions.
15 changes: 7 additions & 8 deletions lib/tapioca/dsl/compilers/rubocop.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion lib/tapioca/dsl/extensions/rubocop.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
30 changes: 17 additions & 13 deletions lib/tapioca/runtime/reflection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) # => []
Expand All @@ -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 "<top (required)>". If none is found, it returns the location
# labeled "<main>", which is the original call site.
Expand Down
82 changes: 50 additions & 32 deletions spec/tapioca/dsl/compilers/rubocop_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 924bcef

Please sign in to comment.