-
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
57e2c6b
commit 6600b63
Showing
5 changed files
with
302 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,66 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
begin | ||
require "rubocop" | ||
rescue LoadError | ||
return | ||
end | ||
|
||
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 { { fixed: T.all(T.class_of(::RuboCop::Cop::Base), Extensions::RuboCop) } } | ||
|
||
class << self | ||
extend T::Sig | ||
sig { override.returns(T::Enumerable[Class]) } | ||
def gather_constants | ||
descendants_of(::RuboCop::Cop::Base).select { |constant| name_of(constant) } | ||
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,45 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
begin | ||
require "rubocop" | ||
rescue LoadError | ||
return | ||
end | ||
|
||
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::Cop::Base.singleton_class.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,166 @@ | ||
# 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 | ||
.descendants_of(::RuboCop::Cop::Base) | ||
.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 "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) | ||
end | ||
|
||
it "gathers constants inheriting RuboCop::Cop::Base in gems" do | ||
# Sample of miscellaneous constants that should be found from Rubocop and plugins | ||
expected_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", | ||
] | ||
|
||
assert_equal(expected_constants, expected_constants & gathered_constants) | ||
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 | ||
class MyLegacyCop < ::RuboCop::Cop::Cop | ||
end | ||
module ::RuboCop | ||
module Cop | ||
module MyApp | ||
class MyNamespacedCop < Base | ||
end | ||
end | ||
end | ||
end | ||
RUBY | ||
|
||
assert_equal( | ||
["MyCop", "MyLegacyCop", "RuboCop::Cop::MyApp::MyNamespacedCop"], | ||
relevant_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 | ||
|
||
sig { returns(T::Array[String]) } | ||
def relevant_gathered_constants | ||
gathered_constants - EXISTING_CONSTANTS | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |