-
Notifications
You must be signed in to change notification settings - Fork 123
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This generates RBI signatures for use of Rubocop's Node Pattern macros (`def_node_matcher` & `def_node_search`).
- Loading branch information
1 parent
e46f9e2
commit dbc15ce
Showing
5 changed files
with
317 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
return unless defined?(RuboCop::AST::NodePattern::Macros) | ||
|
||
module Tapioca | ||
module Dsl | ||
module Compilers | ||
# `Tapioca::Dsl::Compilers::RuboCop` generates types for RuboCop cops. | ||
# RuboCop uses macros to define methods leveraging "AST node patterns". | ||
# For example, in this cop | ||
# | ||
# class MyCop < Base | ||
# def_node_matcher :matches_some_pattern?, "..." | ||
# | ||
# def on_send(node) | ||
# return unless matches_some_pattern?(node) | ||
# # ... | ||
# end | ||
# end | ||
# | ||
# the use of `def_node_matcher` will generate the method | ||
# `matches_some_pattern?`, for which this compiler will generate a `sig`. | ||
# | ||
# More complex uses are also supported, including: | ||
# | ||
# - Usage of `def_node_search` | ||
# - Parameter specification | ||
# - Default parameter specification, including generating sigs for | ||
# `without_defaults_*` methods | ||
class RuboCop < Compiler | ||
ConstantType = type_member do | ||
{ fixed: T.all(Module, Extensions::RuboCop) } | ||
end | ||
|
||
class << self | ||
extend T::Sig | ||
sig { override.returns(T::Array[T.all(Module, Extensions::RuboCop)]) } | ||
def gather_constants | ||
T.cast( | ||
extenders_of(::RuboCop::AST::NodePattern::Macros).select { |constant| name_of(constant) }, | ||
T::Array[T.all(Module, Extensions::RuboCop)], | ||
) | ||
end | ||
end | ||
|
||
sig { override.void } | ||
def decorate | ||
return if node_methods.empty? | ||
|
||
root.create_path(constant) do |cop_klass| | ||
node_methods.each do |name| | ||
create_method_from_def(cop_klass, constant.instance_method(name)) | ||
end | ||
end | ||
end | ||
|
||
private | ||
|
||
sig { returns(T::Array[Extensions::RuboCop::MethodName]) } | ||
def node_methods | ||
constant.__tapioca_node_methods | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
return unless defined?(RuboCop::AST::NodePattern::Macros) | ||
|
||
module Tapioca | ||
module Dsl | ||
module Compilers | ||
module Extensions | ||
module RuboCop | ||
extend T::Sig | ||
|
||
MethodName = T.type_alias { T.any(String, Symbol) } | ||
|
||
sig { params(name: MethodName, _args: T.untyped, defaults: T.untyped).returns(MethodName) } | ||
def def_node_matcher(name, *_args, **defaults) | ||
__tapioca_node_methods << name | ||
__tapioca_node_methods << :"without_defaults_#{name}" unless defaults.empty? | ||
|
||
super | ||
end | ||
|
||
sig { params(name: MethodName, _args: T.untyped, defaults: T.untyped).returns(MethodName) } | ||
def def_node_search(name, *_args, **defaults) | ||
__tapioca_node_methods << name | ||
__tapioca_node_methods << :"without_defaults_#{name}" unless defaults.empty? | ||
|
||
super | ||
end | ||
|
||
sig { returns(T::Array[MethodName]) } | ||
def __tapioca_node_methods | ||
@__tapioca_node_methods ||= T.let([], T.nilable(T::Array[MethodName])) | ||
end | ||
|
||
::RuboCop::AST::NodePattern::Macros.prepend(self) | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
## RuboCop | ||
|
||
`Tapioca::Dsl::Compilers::RuboCop` generates types for RuboCop cops. | ||
RuboCop uses macros to define methods leveraging "AST node patterns". | ||
For example, in this cop | ||
|
||
class MyCop < Base | ||
def_node_matcher :matches_some_pattern?, "..." | ||
|
||
def on_send(node) | ||
return unless matches_some_pattern?(node) | ||
# ... | ||
end | ||
end | ||
|
||
the use of `def_node_matcher` will generate the method | ||
`matches_some_pattern?`, for which this compiler will generate a `sig`. | ||
|
||
More complex uses are also supported, including: | ||
|
||
- Usage of `def_node_search` | ||
- Parameter specification | ||
- Default parameter specification, including generating sigs for | ||
`without_defaults_*` methods |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
require "spec_helper" | ||
# 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 | ||
# .extenders_of(::RuboCop::AST::NodePattern::Macros) | ||
# .filter_map { |constant| Runtime::Reflection.name_of(constant) }, | ||
# T::Array[String], | ||
# ) | ||
|
||
class << self | ||
extend T::Sig | ||
|
||
sig { override.returns(String) } | ||
def target_class_file | ||
# Against convention, RuboCop uses "rubocop" in its file names, so we do too. | ||
super.gsub("rubo_cop", "rubocop") | ||
end | ||
end | ||
|
||
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 | ||
gathered_constants = gather_constants do | ||
add_ruby_file("content.rb", <<~RUBY) | ||
class Unrelated | ||
end | ||
RUBY | ||
end | ||
assert_empty(gathered_constants) | ||
end | ||
|
||
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", | ||
"RuboCop::Cop::Cop", | ||
"RuboCop::Cop::Gemspec::DependencyVersion", | ||
"RuboCop::Cop::Lint::Void", | ||
"RuboCop::Cop::Metrics::ClassLength", | ||
"RuboCop::Cop::Migration::DepartmentName", | ||
"RuboCop::Cop::Naming::MethodName", | ||
"RuboCop::Cop::Security::CompoundHash", | ||
"RuboCop::Cop::Sorbet::ValidSigil", | ||
"RuboCop::Cop::Style::YodaCondition", | ||
] - gathered_constants | ||
|
||
assert_empty(missing_constants, "expected constants to be gathered") | ||
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 | ||
module MyMacroModule | ||
extend ::RuboCop::AST::NodePattern::Macros | ||
end | ||
module ::RuboCop | ||
module Cop | ||
module MyApp | ||
class MyNamespacedCop < Base | ||
end | ||
end | ||
end | ||
end | ||
RUBY | ||
end | ||
|
||
assert_equal( | ||
["MyCop", "MyLegacyCop", "MyMacroModule", "RuboCop::Cop::MyApp::MyNamespacedCop"], | ||
gathered_constants, | ||
) | ||
end | ||
end | ||
|
||
describe "decorate" do | ||
it "generates empty RBI when no DSL used" do | ||
add_ruby_file("content.rb", <<~RUBY) | ||
class MyCop < ::RuboCop::Cop::Base | ||
def on_send(node);end | ||
end | ||
RUBY | ||
|
||
expected = <<~RBI | ||
# typed: strong | ||
RBI | ||
|
||
assert_equal(expected, rbi_for(:MyCop)) | ||
end | ||
|
||
it "generates correct RBI file" do | ||
add_ruby_file("content.rb", <<~RUBY) | ||
class MyCop < ::RuboCop::Cop::Base | ||
def_node_matcher :some_matcher, "(...)" | ||
def_node_matcher :some_matcher_with_params, "(%1 %two ...)" | ||
def_node_matcher :some_matcher_with_params_and_defaults, "(%1 %two ...)", two: :default | ||
def_node_matcher :some_predicate_matcher?, "(...)" | ||
def_node_search :some_search, "(...)" | ||
def_node_search :some_search_with_params, "(%1 %two ...)" | ||
def_node_search :some_search_with_params_and_defaults, "(%1 %two ...)", two: :default | ||
def on_send(node);end | ||
end | ||
RUBY | ||
|
||
expected = <<~RBI | ||
# typed: strong | ||
class MyCop | ||
sig { params(param0: T.untyped).returns(T.untyped) } | ||
def some_matcher(param0 = T.unsafe(nil)); end | ||
sig { params(param0: T.untyped, param1: T.untyped, two: T.untyped).returns(T.untyped) } | ||
def some_matcher_with_params(param0 = T.unsafe(nil), param1, two:); end | ||
sig { params(args: T.untyped, values: T.untyped).returns(T.untyped) } | ||
def some_matcher_with_params_and_defaults(*args, **values); end | ||
sig { params(param0: T.untyped).returns(T.untyped) } | ||
def some_predicate_matcher?(param0 = T.unsafe(nil)); end | ||
sig { params(param0: T.untyped).returns(T.untyped) } | ||
def some_search(param0); end | ||
sig { params(param0: T.untyped, param1: T.untyped, two: T.untyped).returns(T.untyped) } | ||
def some_search_with_params(param0, param1, two:); end | ||
sig { params(args: T.untyped, values: T.untyped).returns(T.untyped) } | ||
def some_search_with_params_and_defaults(*args, **values); end | ||
sig { params(param0: T.untyped, param1: T.untyped, two: T.untyped).returns(T.untyped) } | ||
def without_defaults_some_matcher_with_params_and_defaults(param0 = T.unsafe(nil), param1, two:); end | ||
sig { params(param0: T.untyped, param1: T.untyped, two: T.untyped).returns(T.untyped) } | ||
def without_defaults_some_search_with_params_and_defaults(param0, param1, two:); end | ||
end | ||
RBI | ||
|
||
assert_equal(expected, rbi_for(:MyCop)) | ||
end | ||
end | ||
|
||
private | ||
|
||
# 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 | ||
end | ||
end | ||
end |