sorbet-rails 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +13 -1
- data/.travis.yml +2 -2
- data/Gemfile +1 -1
- data/README.md +79 -3
- data/lib/sorbet-rails.rb +5 -1
- data/lib/sorbet-rails/activerecord.rbi +27 -0
- data/lib/sorbet-rails/custom_finder_methods.rb +11 -0
- data/lib/sorbet-rails/helper_rbi_formatter.rb +33 -0
- data/lib/sorbet-rails/model_plugins/active_record_assoc.rb +91 -0
- data/lib/sorbet-rails/model_plugins/active_record_attribute.rb +111 -0
- data/lib/sorbet-rails/model_plugins/active_record_enum.rb +43 -0
- data/lib/sorbet-rails/model_plugins/active_record_finder_methods.rb +80 -0
- data/lib/sorbet-rails/model_plugins/active_record_named_scope.rb +28 -0
- data/lib/sorbet-rails/model_plugins/active_record_querying.rb +42 -0
- data/lib/sorbet-rails/model_plugins/active_relation_where_not.rb +28 -0
- data/lib/sorbet-rails/model_plugins/base.rb +33 -0
- data/lib/sorbet-rails/model_plugins/custom_finder_methods.rb +54 -0
- data/lib/sorbet-rails/model_plugins/enumerable_collections.rb +49 -0
- data/lib/sorbet-rails/model_plugins/plugins.rb +45 -0
- data/lib/sorbet-rails/model_rbi_formatter.rb +108 -362
- data/lib/sorbet-rails/model_utils.rb +45 -0
- data/lib/sorbet-rails/railtie.rb +1 -0
- data/lib/sorbet-rails/routes_rbi_formatter.rb +12 -3
- data/lib/sorbet-rails/tasks/rails_rbi.rake +36 -15
- data/lib/sorbet-rails/utils.rb +4 -0
- data/sorbet-rails.gemspec +4 -2
- data/spec/helper_rbi_formatter_spec.rb +13 -0
- data/spec/model_plugins_spec.rb +31 -0
- data/spec/model_rbi_formatter_spec.rb +5 -5
- data/spec/rake_rails_rbi_helpers_spec.rb +14 -0
- data/spec/routes_rbi_formatter_spec.rb +3 -3
- data/spec/sorbet_spec.rb +12 -16
- data/spec/support/rails_shared/app/controllers/application_controller.rb +1 -1
- data/spec/support/rails_shared/app/helpers/bar_helper.rb +2 -0
- data/spec/support/rails_shared/app/helpers/baz_helper.rb +2 -0
- data/spec/support/rails_shared/app/helpers/foo_helper.rb +2 -0
- data/spec/support/rails_shared/app/models/concerns/mythical.rb +10 -0
- data/spec/support/rails_shared/app/models/wand.rb +1 -0
- data/spec/support/rails_shared/config/initializers/sorbet_rails.rb +3 -0
- data/spec/support/rails_shared/lib/mythical_rbi_plugin.rb +16 -0
- data/spec/support/rails_shared/sorbet_test_cases.rb +5 -2
- data/spec/support/rails_shared/typed-override.yaml +2 -0
- data/spec/support/rails_symlinks/app/helpers +1 -0
- data/spec/support/rails_symlinks/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/rails_symlinks/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/rails_symlinks/typed-override.yaml +1 -0
- data/spec/support/v4.2/Gemfile.lock +5 -0
- data/spec/support/v4.2/app/helpers +1 -0
- data/spec/support/v4.2/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/v4.2/config/routes.rb +0 -1
- data/spec/support/v4.2/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/v4.2/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
- data/spec/support/v4.2/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
- data/spec/support/v4.2/typed-override.yaml +1 -0
- data/spec/support/v5.0/Gemfile.lock +5 -0
- data/spec/support/v5.0/app/helpers +1 -0
- data/spec/support/v5.0/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/v5.0/config/routes.rb +0 -1
- data/spec/support/v5.0/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/v5.0/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
- data/spec/support/v5.0/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
- data/spec/support/v5.0/typed-override.yaml +1 -0
- data/spec/support/v5.1/Gemfile.lock +5 -0
- data/spec/support/v5.1/app/helpers +1 -0
- data/spec/support/v5.1/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/v5.1/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/v5.1/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
- data/spec/support/v5.1/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
- data/spec/support/v5.1/typed-override.yaml +1 -0
- data/spec/support/v5.2-no-sorbet/Gemfile +0 -2
- data/spec/support/v5.2-no-sorbet/Gemfile.lock +7 -5
- data/spec/support/v5.2-no-sorbet/app/helpers +1 -0
- data/spec/support/v5.2-no-sorbet/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/v5.2-no-sorbet/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/v5.2-no-sorbet/sorbet_test_cases.rb +1 -0
- data/spec/support/v5.2-no-sorbet/typed-override.yaml +1 -0
- data/spec/support/v5.2/Gemfile +0 -2
- data/spec/support/v5.2/Gemfile.lock +7 -5
- data/spec/support/v5.2/app/helpers +1 -0
- data/spec/support/v5.2/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/v5.2/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/v5.2/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
- data/spec/support/v5.2/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
- data/spec/support/v5.2/typed-override.yaml +1 -0
- data/spec/support/v6.0/Gemfile +1 -3
- data/spec/support/v6.0/Gemfile.lock +61 -59
- data/spec/support/v6.0/app/helpers +1 -0
- data/spec/support/v6.0/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/v6.0/config/routes.rb +0 -1
- data/spec/support/v6.0/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/v6.0/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
- data/spec/support/v6.0/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
- data/spec/support/v6.0/typed-override.yaml +1 -0
- data/spec/test_data/v4.2/expected_helpers.rbi +17 -0
- data/spec/test_data/v4.2/expected_potion.rbi +259 -28
- data/spec/test_data/v4.2/expected_spell_book.rbi +278 -37
- data/spec/test_data/v4.2/expected_srb_tc_output.txt +1 -99
- data/spec/test_data/v4.2/expected_wand.rbi +345 -96
- data/spec/test_data/v4.2/expected_wizard.rbi +322 -76
- data/spec/test_data/v4.2/expected_wizard_wo_spellbook.rbi +322 -76
- data/spec/test_data/v5.0/expected_helpers.rbi +17 -0
- data/spec/test_data/v5.0/expected_internal_metadata.rbi +273 -37
- data/spec/test_data/v5.0/expected_potion.rbi +259 -28
- data/spec/test_data/v5.0/expected_schema_migration.rbi +264 -28
- data/spec/test_data/v5.0/expected_spell_book.rbi +278 -37
- data/spec/test_data/v5.0/expected_srb_tc_output.txt +1 -81
- data/spec/test_data/v5.0/expected_wand.rbi +341 -92
- data/spec/test_data/v5.0/expected_wizard.rbi +318 -72
- data/spec/test_data/v5.0/expected_wizard_wo_spellbook.rbi +318 -72
- data/spec/test_data/v5.1/expected_helpers.rbi +17 -0
- data/spec/test_data/v5.1/expected_internal_metadata.rbi +273 -37
- data/spec/test_data/v5.1/expected_potion.rbi +259 -28
- data/spec/test_data/v5.1/expected_schema_migration.rbi +264 -28
- data/spec/test_data/v5.1/expected_spell_book.rbi +278 -37
- data/spec/test_data/v5.1/expected_srb_tc_output.txt +1 -81
- data/spec/test_data/v5.1/expected_wand.rbi +341 -92
- data/spec/test_data/v5.1/expected_wizard.rbi +318 -72
- data/spec/test_data/v5.1/expected_wizard_wo_spellbook.rbi +318 -72
- data/spec/test_data/v5.2-no-sorbet/expected_attachment.rbi +265 -29
- data/spec/test_data/v5.2-no-sorbet/expected_blob.rbi +271 -35
- data/spec/test_data/v5.2-no-sorbet/expected_helpers.rbi +17 -0
- data/spec/test_data/v5.2-no-sorbet/expected_internal_metadata.rbi +273 -37
- data/spec/test_data/v5.2-no-sorbet/expected_potion.rbi +259 -28
- data/spec/test_data/v5.2-no-sorbet/expected_schema_migration.rbi +264 -28
- data/spec/test_data/v5.2-no-sorbet/expected_spell_book.rbi +278 -37
- data/spec/test_data/v5.2-no-sorbet/expected_srb_tc_output.txt +1 -81
- data/spec/test_data/v5.2-no-sorbet/expected_wand.rbi +351 -102
- data/spec/test_data/v5.2-no-sorbet/expected_wizard.rbi +318 -72
- data/spec/test_data/v5.2-no-sorbet/expected_wizard_wo_spellbook.rbi +318 -72
- data/spec/test_data/v5.2/expected_attachment.rbi +265 -29
- data/spec/test_data/v5.2/expected_blob.rbi +271 -35
- data/spec/test_data/v5.2/expected_helpers.rbi +17 -0
- data/spec/test_data/v5.2/expected_internal_metadata.rbi +273 -37
- data/spec/test_data/v5.2/expected_potion.rbi +259 -28
- data/spec/test_data/v5.2/expected_schema_migration.rbi +264 -28
- data/spec/test_data/v5.2/expected_spell_book.rbi +278 -37
- data/spec/test_data/v5.2/expected_srb_tc_output.txt +1 -81
- data/spec/test_data/v5.2/expected_wand.rbi +351 -102
- data/spec/test_data/v5.2/expected_wizard.rbi +318 -72
- data/spec/test_data/v5.2/expected_wizard_wo_spellbook.rbi +318 -72
- data/spec/test_data/v6.0/expected_attachment.rbi +265 -29
- data/spec/test_data/v6.0/expected_blob.rbi +271 -35
- data/spec/test_data/v6.0/expected_helpers.rbi +17 -0
- data/spec/test_data/v6.0/expected_internal_metadata.rbi +273 -37
- data/spec/test_data/v6.0/expected_potion.rbi +259 -28
- data/spec/test_data/v6.0/expected_schema_migration.rbi +264 -28
- data/spec/test_data/v6.0/expected_spell_book.rbi +278 -37
- data/spec/test_data/v6.0/expected_srb_tc_output.txt +1 -81
- data/spec/test_data/v6.0/expected_wand.rbi +351 -102
- data/spec/test_data/v6.0/expected_wizard.rbi +318 -72
- data/spec/test_data/v6.0/expected_wizard_wo_spellbook.rbi +318 -72
- metadata +120 -37
- data/lib/sorbet-rails/rbi/activerecord.rbi +0 -207
- data/spec/support/v4.2/app/helpers/application_helper.rb +0 -3
- data/spec/support/v4.2/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
- data/spec/support/v4.2/sorbet/rbi/hidden-definitions/errors.txt +0 -11998
- data/spec/support/v4.2/sorbet/rbi/hidden-definitions/hidden.rbi +0 -27774
- data/spec/support/v5.0/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
- data/spec/support/v5.0/sorbet/rbi/hidden-definitions/errors.txt +0 -10523
- data/spec/support/v5.0/sorbet/rbi/hidden-definitions/hidden.rbi +0 -24969
- data/spec/support/v5.1/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
- data/spec/support/v5.1/sorbet/rbi/hidden-definitions/errors.txt +0 -10226
- data/spec/support/v5.1/sorbet/rbi/hidden-definitions/hidden.rbi +0 -24635
- data/spec/support/v5.2/.ruby-version +0 -1
- data/spec/support/v5.2/sorbet/rbi/gems/sorbet-runtime.rbi +0 -644
- data/spec/support/v5.2/sorbet/rbi/hidden-definitions/errors.txt +0 -10046
- data/spec/support/v5.2/sorbet/rbi/hidden-definitions/hidden.rbi +0 -24424
- data/spec/support/v6.0/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
- data/spec/support/v6.0/sorbet/rbi/hidden-definitions/errors.txt +0 -12074
- data/spec/support/v6.0/sorbet/rbi/hidden-definitions/hidden.rbi +0 -28231
@@ -0,0 +1,43 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require ('sorbet-rails/model_plugins/base')
|
3
|
+
class SorbetRails::ModelPlugins::ActiveRecordEnum < SorbetRails::ModelPlugins::Base
|
4
|
+
|
5
|
+
sig { implementation.params(root: Parlour::RbiGenerator::Namespace).void }
|
6
|
+
def generate(root)
|
7
|
+
return unless model_class.defined_enums.size > 0
|
8
|
+
|
9
|
+
enum_module_name = model_module_name("EnumInstanceMethods")
|
10
|
+
enum_module_rbi = root.create_module(enum_module_name)
|
11
|
+
enum_module_rbi.create_extend("T::Sig")
|
12
|
+
|
13
|
+
model_class_rbi = root.create_class(self.model_class_name)
|
14
|
+
model_class_rbi.create_include(enum_module_name)
|
15
|
+
|
16
|
+
model_relation_shared_rbi = root.create_module(self.model_relation_shared_module_name)
|
17
|
+
|
18
|
+
# TODO: add any method for signature verification?
|
19
|
+
model_class.defined_enums.sort.each do |enum_name, enum_hash|
|
20
|
+
model_class_rbi.create_method(
|
21
|
+
enum_name.pluralize,
|
22
|
+
return_type: "T::Hash[T.any(String, Symbol), Integer]",
|
23
|
+
class_method: true,
|
24
|
+
)
|
25
|
+
enum_hash.keys.each do |enum_val|
|
26
|
+
enum_module_rbi.create_method(
|
27
|
+
"#{enum_val}?",
|
28
|
+
return_type: "T::Boolean",
|
29
|
+
)
|
30
|
+
enum_module_rbi.create_method(
|
31
|
+
"#{enum_val}!",
|
32
|
+
return_type: nil, # void
|
33
|
+
)
|
34
|
+
# force generating these methods because sorbet's hidden-definitions generate & override them
|
35
|
+
model_class_rbi.create_method(
|
36
|
+
"#{enum_val}",
|
37
|
+
return_type: self.model_relation_class_name,
|
38
|
+
class_method: true,
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require ('sorbet-rails/model_plugins/base')
|
3
|
+
class SorbetRails::ModelPlugins::ActiveRecordFinderMethods < SorbetRails::ModelPlugins::Base
|
4
|
+
|
5
|
+
sig { implementation.params(root: Parlour::RbiGenerator::Namespace).void }
|
6
|
+
def generate(root)
|
7
|
+
model_class_rbi = root.create_class(self.model_class_name)
|
8
|
+
create_finder_methods_for(model_class_rbi, class_method: true)
|
9
|
+
|
10
|
+
model_relation_class_rbi = root.create_class(self.model_relation_class_name)
|
11
|
+
create_finder_methods_for(model_relation_class_rbi, class_method: false)
|
12
|
+
|
13
|
+
model_assoc_proxy_class_rbi = root.create_class(self.model_assoc_proxy_class_name)
|
14
|
+
create_finder_methods_for(model_assoc_proxy_class_rbi, class_method: false)
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { params(class_rbi: Parlour::RbiGenerator::ClassNamespace, class_method: T::Boolean).void }
|
18
|
+
def create_finder_methods_for(class_rbi, class_method:)
|
19
|
+
class_rbi.create_method(
|
20
|
+
"find",
|
21
|
+
parameters: [ Parameter.new("*args", type: "T.untyped") ],
|
22
|
+
return_type: self.model_class_name,
|
23
|
+
class_method: class_method,
|
24
|
+
)
|
25
|
+
class_rbi.create_method(
|
26
|
+
"find_by",
|
27
|
+
parameters: [ Parameter.new("*args", type: "T.untyped") ],
|
28
|
+
return_type: "T.nilable(#{self.model_class_name})",
|
29
|
+
class_method: class_method,
|
30
|
+
)
|
31
|
+
class_rbi.create_method(
|
32
|
+
"find_by!",
|
33
|
+
parameters: [ Parameter.new("*args", type: "T.untyped") ],
|
34
|
+
return_type: self.model_class_name,
|
35
|
+
class_method: class_method
|
36
|
+
)
|
37
|
+
|
38
|
+
["first", "second", "third", "third_to_last", "second_to_last", "last"].
|
39
|
+
each do |method_name|
|
40
|
+
create_finder_method_pair(class_rbi, method_name, class_method)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Checker methods
|
44
|
+
class_rbi.create_method(
|
45
|
+
"exists?",
|
46
|
+
parameters: [ Parameter.new("conditions", type: "T.untyped", default: "nil") ],
|
47
|
+
return_type: "T::Boolean",
|
48
|
+
class_method: class_method,
|
49
|
+
)
|
50
|
+
["any?", "many?", "none?", "one?"].each do |method_name|
|
51
|
+
class_rbi.create_method(
|
52
|
+
method_name,
|
53
|
+
parameters: [ Parameter.new("*args", type: "T.untyped") ],
|
54
|
+
return_type: "T::Boolean",
|
55
|
+
class_method: class_method,
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
sig {
|
61
|
+
params(
|
62
|
+
class_rbi: Parlour::RbiGenerator::ClassNamespace,
|
63
|
+
method_name: String,
|
64
|
+
class_method: T::Boolean,
|
65
|
+
).
|
66
|
+
void
|
67
|
+
}
|
68
|
+
def create_finder_method_pair(class_rbi, method_name, class_method)
|
69
|
+
class_rbi.create_method(
|
70
|
+
method_name,
|
71
|
+
return_type: "T.nilable(#{self.model_class_name})",
|
72
|
+
class_method: class_method,
|
73
|
+
)
|
74
|
+
class_rbi.create_method(
|
75
|
+
"#{method_name}!",
|
76
|
+
return_type: self.model_class_name,
|
77
|
+
class_method: class_method,
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require ('sorbet-rails/model_plugins/base')
|
3
|
+
class SorbetRails::ModelPlugins::ActiveRecordNamedScope < SorbetRails::ModelPlugins::Base
|
4
|
+
|
5
|
+
sig { implementation.params(root: Parlour::RbiGenerator::Namespace).void }
|
6
|
+
def generate(root)
|
7
|
+
ar_named_scope_rbi = root.create_module(self.model_relation_shared_module_name)
|
8
|
+
@model_class.methods.sort.each do |method_name|
|
9
|
+
method_obj = @model_class.method(method_name)
|
10
|
+
next unless method_obj.present? && method_obj.source_location.present?
|
11
|
+
# we detect sscopes defined in a model by 2 criteria:
|
12
|
+
# - they don't have an owner name
|
13
|
+
# - they are defined in 'activerecord/lib/active_record/scoping/named.rb'
|
14
|
+
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/scoping/named.rb
|
15
|
+
next unless method_obj.owner.name == nil
|
16
|
+
source_file = method_obj.source_location[0]
|
17
|
+
next unless source_file.include?("lib/active_record/scoping/named.rb")
|
18
|
+
|
19
|
+
ar_named_scope_rbi.create_method(
|
20
|
+
method_name.to_s,
|
21
|
+
parameters: [
|
22
|
+
Parameter.new("*args", type: "T.untyped"),
|
23
|
+
],
|
24
|
+
return_type: self.model_relation_class_name,
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require ('sorbet-rails/model_plugins/base')
|
3
|
+
class SorbetRails::ModelPlugins::ActiveRecordQuerying < SorbetRails::ModelPlugins::Base
|
4
|
+
|
5
|
+
sig { implementation.params(root: Parlour::RbiGenerator::Namespace).void }
|
6
|
+
def generate(root)
|
7
|
+
# All is a named scope that most method from ActiveRecord::Querying delegate to
|
8
|
+
# rails/activerecord/lib/active_record/querying.rb:21
|
9
|
+
ar_querying_rbi = root.create_module(self.model_relation_shared_module_name)
|
10
|
+
ar_querying_rbi.create_method(
|
11
|
+
"all",
|
12
|
+
return_type: self.model_relation_class_name,
|
13
|
+
)
|
14
|
+
ar_querying_rbi.create_method(
|
15
|
+
"unscoped",
|
16
|
+
parameters: [
|
17
|
+
Parameter.new("&block", type: "T.nilable(T.proc.void)"),
|
18
|
+
],
|
19
|
+
return_type: self.model_relation_class_name,
|
20
|
+
)
|
21
|
+
|
22
|
+
# It's not possible to typedef all methods in ActiveRecord::Querying module to have the
|
23
|
+
# matching type. By generating model-specific sig, we can typedef these methods to return
|
24
|
+
# <Model>::Relation class.
|
25
|
+
# rails/activerecord/lib/active_record/querying.rb
|
26
|
+
model_query_relation_methods = [
|
27
|
+
:select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
|
28
|
+
:where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
|
29
|
+
:having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
|
30
|
+
]
|
31
|
+
model_query_relation_methods.each do |method_name|
|
32
|
+
ar_querying_rbi.create_method(
|
33
|
+
method_name.to_s,
|
34
|
+
parameters: [
|
35
|
+
Parameter.new("*args", type: "T.untyped"),
|
36
|
+
Parameter.new("&block", type: "T.nilable(T.proc.void)"),
|
37
|
+
],
|
38
|
+
return_type: self.model_relation_class_name,
|
39
|
+
) if exists_class_method?(method_name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require ('sorbet-rails/model_plugins/base')
|
3
|
+
class SorbetRails::ModelPlugins::ActiveRelationWhereNot < SorbetRails::ModelPlugins::Base
|
4
|
+
|
5
|
+
sig { implementation.params(root: Parlour::RbiGenerator::Namespace).void }
|
6
|
+
def generate(root)
|
7
|
+
where_not_module_name = self.model_module_name("ActiveRelation_WhereNot")
|
8
|
+
where_not_module_rbi = root.create_module(where_not_module_name)
|
9
|
+
|
10
|
+
model_relation_class_rbi = root.create_class(self.model_relation_class_name)
|
11
|
+
model_relation_class_rbi.create_include(where_not_module_name)
|
12
|
+
|
13
|
+
model_assoc_proxy_class_rbi = root.create_class(self.model_assoc_proxy_class_name)
|
14
|
+
model_assoc_proxy_class_rbi.create_include(where_not_module_name)
|
15
|
+
|
16
|
+
# TODO: where.not is a special case that we replace it with a `where_not` method
|
17
|
+
# `where` when not given parameters will return a `ActiveRecord::QueryMethods::WhereChain`
|
18
|
+
# instance that has a method `not` on it
|
19
|
+
where_not_module_rbi.create_method(
|
20
|
+
"not",
|
21
|
+
parameters: [
|
22
|
+
Parameter.new("opts", type: "T.untyped", default: nil),
|
23
|
+
Parameter.new("*rest", type: "T.untyped", default: nil),
|
24
|
+
],
|
25
|
+
return_type: "T.self_type",
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require('parlour')
|
3
|
+
require('sorbet-rails/model_utils')
|
4
|
+
module SorbetRails::ModelPlugins
|
5
|
+
class Base < ::Parlour::Plugin
|
6
|
+
extend T::Sig
|
7
|
+
extend T::Helpers
|
8
|
+
include SorbetRails::ModelUtils
|
9
|
+
|
10
|
+
abstract!
|
11
|
+
|
12
|
+
# convenient rename
|
13
|
+
Parameter = ::Parlour::RbiGenerator::Parameter
|
14
|
+
|
15
|
+
sig { implementation.returns(T.class_of(ActiveRecord::Base)) }
|
16
|
+
attr_reader :model_class
|
17
|
+
|
18
|
+
sig { returns(T::Set[String]) }
|
19
|
+
attr_reader :available_classes
|
20
|
+
|
21
|
+
sig {
|
22
|
+
params(
|
23
|
+
model_class: T.class_of(ActiveRecord::Base),
|
24
|
+
available_classes: T::Set[String],
|
25
|
+
).
|
26
|
+
void
|
27
|
+
}
|
28
|
+
def initialize(model_class, available_classes)
|
29
|
+
@model_class = T.let(model_class, T.class_of(ActiveRecord::Base))
|
30
|
+
@available_classes = T.let(available_classes, T::Set[String])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require ('sorbet-rails/model_plugins/base')
|
3
|
+
class SorbetRails::ModelPlugins::CustomFinderMethods < SorbetRails::ModelPlugins::Base
|
4
|
+
|
5
|
+
sig { implementation.params(root: Parlour::RbiGenerator::Namespace).void }
|
6
|
+
def generate(root)
|
7
|
+
model_class_rbi = root.create_class(self.model_class_name)
|
8
|
+
model_relation_class_rbi = root.create_class(self.model_relation_class_name)
|
9
|
+
model_assoc_proxy_class_rbi = root.create_class(self.model_assoc_proxy_class_name)
|
10
|
+
|
11
|
+
# include the actual module
|
12
|
+
model_class_rbi.create_extend("SorbetRails::CustomFinderMethods")
|
13
|
+
model_relation_class_rbi.create_include("SorbetRails::CustomFinderMethods")
|
14
|
+
model_assoc_proxy_class_rbi.create_include("SorbetRails::CustomFinderMethods")
|
15
|
+
|
16
|
+
custom_module_name = self.model_module_name("CustomFinderMethods")
|
17
|
+
custom_module_rbi = root.create_module(custom_module_name)
|
18
|
+
|
19
|
+
# and include the rbi module
|
20
|
+
model_class_rbi.create_extend(custom_module_name)
|
21
|
+
model_relation_class_rbi.create_include(custom_module_name)
|
22
|
+
model_assoc_proxy_class_rbi.create_include(custom_module_name)
|
23
|
+
|
24
|
+
custom_module_rbi.create_method(
|
25
|
+
"first_n",
|
26
|
+
parameters: [ Parameter.new("limit", type: "Integer") ],
|
27
|
+
return_type: "T::Array[#{self.model_class_name}]",
|
28
|
+
)
|
29
|
+
|
30
|
+
custom_module_rbi.create_method(
|
31
|
+
"last_n",
|
32
|
+
parameters: [ Parameter.new("limit", type: "Integer") ],
|
33
|
+
return_type: "T::Array[#{self.model_class_name}]",
|
34
|
+
)
|
35
|
+
|
36
|
+
custom_module_rbi.create_method(
|
37
|
+
"find_n",
|
38
|
+
parameters: [ Parameter.new("*args", type: "T::Array[T.any(Integer, String)]") ],
|
39
|
+
return_type: "T::Array[#{self.model_class_name}]",
|
40
|
+
)
|
41
|
+
|
42
|
+
# allow common cases find_by_id
|
43
|
+
custom_module_rbi.create_method(
|
44
|
+
"find_by_id",
|
45
|
+
parameters: [ Parameter.new("id", type: "Integer") ],
|
46
|
+
return_type: "T.nilable(#{self.model_class_name})",
|
47
|
+
)
|
48
|
+
custom_module_rbi.create_method(
|
49
|
+
"find_by_id!",
|
50
|
+
parameters: [ Parameter.new("id", type: "Integer") ],
|
51
|
+
return_type: self.model_class_name,
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require ('sorbet-rails/model_plugins/base')
|
3
|
+
class SorbetRails::ModelPlugins::EnumerableCollections < SorbetRails::ModelPlugins::Base
|
4
|
+
|
5
|
+
sig { implementation.params(root: Parlour::RbiGenerator::Namespace).void }
|
6
|
+
def generate(root)
|
7
|
+
# model relation & association proxy are enumerable
|
8
|
+
# we need to implement "each" in these methods so that they work
|
9
|
+
model_relation_class_rbi = root.create_class(self.model_relation_class_name)
|
10
|
+
create_enumerable_methods_for(model_relation_class_rbi)
|
11
|
+
|
12
|
+
model_assoc_proxy_class_rbi = root.create_class(self.model_assoc_proxy_class_name)
|
13
|
+
create_enumerable_methods_for(model_assoc_proxy_class_rbi)
|
14
|
+
|
15
|
+
# following methods only exists in an association proxy
|
16
|
+
["<<", "append", "push", "concat"].each do |method_name|
|
17
|
+
elem = self.model_class_name
|
18
|
+
model_assoc_proxy_class_rbi.create_method(
|
19
|
+
method_name,
|
20
|
+
parameters: [
|
21
|
+
Parameter.new("*records", type: "T.any(#{elem}, T::Array[#{elem}])"),
|
22
|
+
],
|
23
|
+
return_type: "T.self_type",
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { params(class_rbi: Parlour::RbiGenerator::ClassNamespace).void }
|
29
|
+
def create_enumerable_methods_for(class_rbi)
|
30
|
+
class_rbi.create_include("Enumerable")
|
31
|
+
class_rbi.create_method(
|
32
|
+
"each",
|
33
|
+
parameters: [
|
34
|
+
Parameter.new("&block", type: "T.proc.params(e: #{self.model_class_name}).void")
|
35
|
+
],
|
36
|
+
implementation: true,
|
37
|
+
)
|
38
|
+
class_rbi.create_method(
|
39
|
+
"flatten",
|
40
|
+
parameters: [ Parameter.new("level", type: "T.nilable(Integer)") ],
|
41
|
+
return_type: "T::Array[#{self.model_class_name}]",
|
42
|
+
)
|
43
|
+
# this is an escape hatch when there are conflicts in signatures of Enumerable & ActiveRecord
|
44
|
+
class_rbi.create_method(
|
45
|
+
"to_a",
|
46
|
+
return_type: "T::Array[#{self.model_class_name}]",
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# typed: true
|
2
|
+
require('sorbet-rails/model_plugins/base')
|
3
|
+
require('sorbet-rails/model_plugins/active_record_enum')
|
4
|
+
require('sorbet-rails/model_plugins/active_record_querying')
|
5
|
+
require('sorbet-rails/model_plugins/active_relation_where_not')
|
6
|
+
require('sorbet-rails/model_plugins/active_record_named_scope')
|
7
|
+
require('sorbet-rails/model_plugins/active_record_attribute')
|
8
|
+
require('sorbet-rails/model_plugins/active_record_assoc')
|
9
|
+
require('sorbet-rails/model_plugins/active_record_finder_methods')
|
10
|
+
require('sorbet-rails/model_plugins/custom_finder_methods')
|
11
|
+
require('sorbet-rails/model_plugins/enumerable_collections')
|
12
|
+
|
13
|
+
module SorbetRails::ModelPlugins
|
14
|
+
extend T::Sig
|
15
|
+
|
16
|
+
@@plugins = T.let(
|
17
|
+
[
|
18
|
+
ActiveRecordEnum,
|
19
|
+
ActiveRecordNamedScope,
|
20
|
+
ActiveRecordQuerying,
|
21
|
+
ActiveRelationWhereNot,
|
22
|
+
ActiveRecordAttribute,
|
23
|
+
ActiveRecordAssoc,
|
24
|
+
ActiveRecordFinderMethods,
|
25
|
+
CustomFinderMethods,
|
26
|
+
EnumerableCollections,
|
27
|
+
],
|
28
|
+
T::Array[T.class_of(Base)]
|
29
|
+
)
|
30
|
+
|
31
|
+
sig { params(plugin: T.class_of(Base)).void }
|
32
|
+
def register_plugin(plugin)
|
33
|
+
@@plugins.push(plugin) unless @@plugins.include?(plugin)
|
34
|
+
end
|
35
|
+
|
36
|
+
sig { params(plugins: T::Array[T.class_of(Base)]).void }
|
37
|
+
def set_plugins(plugins)
|
38
|
+
@@plugins = plugins
|
39
|
+
end
|
40
|
+
|
41
|
+
sig { returns(T::Array[T.class_of(Base)]) }
|
42
|
+
def get_plugins
|
43
|
+
@@plugins
|
44
|
+
end
|
45
|
+
end
|
@@ -1,387 +1,133 @@
|
|
1
|
-
# typed:
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
# typed: strict
|
2
|
+
require('parlour')
|
3
|
+
require('sorbet-rails/model_utils')
|
4
|
+
require('sorbet-rails/model_plugins/plugins')
|
5
|
+
|
6
|
+
class SorbetRails::ModelRbiFormatter
|
7
|
+
extend T::Sig
|
8
|
+
extend SorbetRails::ModelPlugins
|
9
|
+
include SorbetRails::ModelUtils
|
10
|
+
|
11
|
+
sig { implementation.returns(T.class_of(ActiveRecord::Base)) }
|
12
|
+
attr_reader :model_class
|
13
|
+
|
14
|
+
sig { returns(T::Set[String]) }
|
15
|
+
attr_reader :available_classes
|
16
|
+
|
17
|
+
sig {
|
18
|
+
params(
|
19
|
+
model_class: T.class_of(ActiveRecord::Base),
|
20
|
+
available_classes: T::Set[String],
|
21
|
+
).
|
22
|
+
void
|
23
|
+
}
|
7
24
|
def initialize(model_class, available_classes)
|
8
|
-
@model_class = model_class
|
9
|
-
@available_classes = available_classes
|
10
|
-
@columns_hash = model_class.table_exists? ? model_class.columns_hash : {}
|
11
|
-
@generated_instance_module_sigs = ActiveSupport::HashWithIndifferentAccess.new
|
12
|
-
@generated_instance_sigs = ActiveSupport::HashWithIndifferentAccess.new
|
13
|
-
@generated_class_sigs = ActiveSupport::HashWithIndifferentAccess.new
|
14
|
-
@generated_scope_sigs = ActiveSupport::HashWithIndifferentAccess.new
|
15
|
-
@generated_querying_sigs = ActiveSupport::HashWithIndifferentAccess.new
|
16
|
-
@model_relation_class_name = "#{@model_class.name}::ActiveRecord_Relation"
|
25
|
+
@model_class = T.let(model_class, T.class_of(ActiveRecord::Base))
|
26
|
+
@available_classes = T.let(available_classes, T::Set[String])
|
17
27
|
begin
|
18
28
|
# Load all dynamic instance methods of this model by instantiating a fake model
|
19
29
|
@model_class.new unless @model_class.abstract_class?
|
20
|
-
rescue StandardError
|
21
|
-
puts "Note: Unable to create new instance of #{model_class.name}"
|
30
|
+
rescue StandardError => err
|
31
|
+
puts "#{err.class}: Note: Unable to create new instance of #{model_class.name}"
|
22
32
|
end
|
23
33
|
end
|
24
34
|
|
35
|
+
sig {returns(String)}
|
25
36
|
def generate_rbi
|
26
37
|
puts "-- Generate sigs for #{@model_class.name} --"
|
27
|
-
populate_activerecord_querying_methods
|
28
|
-
populate_named_scope_methods
|
29
|
-
populate_generated_column_methods
|
30
|
-
populate_generated_association_methods
|
31
|
-
populate_generated_enum_methods
|
32
|
-
|
33
|
-
@buffer = []
|
34
|
-
@buffer << draw_file_header_and_base_classes
|
35
|
-
|
36
|
-
@buffer << draw_module_header("#{@model_class.name}::#{MODEL_INSTANCE_MODULE_SUFFIX}")
|
37
|
-
@model_class.instance_methods.sort.each do |method_name|
|
38
|
-
expected_sig = @generated_instance_module_sigs[method_name]
|
39
|
-
next unless expected_sig.present?
|
40
|
-
method_obj = @model_class.instance_method(method_name)
|
41
|
-
draw_method(method_name, method_obj, expected_sig)
|
42
|
-
end
|
43
|
-
@buffer << draw_module_or_class_footer
|
44
|
-
|
45
|
-
# TODO Enum methods need to be defined under the class definition because sorbet generates them
|
46
|
-
# in the hidden-definition.rbi
|
47
|
-
# When this issue is resolved, they might go away by running `srb rbi hidden-definitions`
|
48
|
-
# This is a sure way to make it work though.
|
49
|
-
# https://github.com/sorbet/sorbet/issues/1161
|
50
|
-
@buffer << draw_class_header("#{@model_class.name}") # ::#{MODEL_CLASS_MODULE_SUFFIX}")
|
51
|
-
@model_class.instance_methods.sort.each do |method_name|
|
52
|
-
expected_sig = @generated_instance_sigs[method_name]
|
53
|
-
next unless expected_sig.present?
|
54
|
-
method_obj = @model_class.instance_method(method_name)
|
55
|
-
draw_method(method_name, method_obj, expected_sig)
|
56
|
-
end
|
57
|
-
@model_class.methods.sort.each do |method_name|
|
58
|
-
expected_sig = @generated_class_sigs[method_name]
|
59
|
-
next unless expected_sig.present?
|
60
|
-
method_obj = @model_class.method(method_name)
|
61
|
-
draw_method(method_name, method_obj, expected_sig, is_class_method: true)
|
62
|
-
end
|
63
|
-
@buffer << draw_module_or_class_footer
|
64
|
-
|
65
|
-
# <Model>::MODEL_RELATION_SHARED_MODULE_SUFFIX is a fake module added so that
|
66
|
-
# when a method is defined in this module, it'll be added to both the Model class
|
67
|
-
# as a class method and to its relation as an instance method.
|
68
|
-
#
|
69
|
-
# We need to define the module after the other classes
|
70
|
-
# to work around Sorbet loading order bug
|
71
|
-
# https://sorbet-ruby.slack.com/archives/CHN2L03NH/p1556065791047300
|
72
|
-
@buffer << draw_module_header("#{@model_class.name}::#{MODEL_RELATION_SHARED_MODULE_SUFFIX}")
|
73
|
-
# For simplicity, generate both in the same module for now.
|
74
|
-
# We don't need to define two fake modules to share methods between <Model> and <Relation>
|
75
|
-
({}.
|
76
|
-
merge(@generated_scope_sigs).
|
77
|
-
merge(@generated_querying_sigs)
|
78
|
-
).each do |method_name, expected_sig|
|
79
|
-
method_obj = @model_class.method(method_name) if @model_class.methods.include?(method_name.to_sym)
|
80
|
-
# this is not a class method because it is added to a module
|
81
|
-
draw_method(method_name, method_obj, expected_sig)
|
82
|
-
end
|
83
|
-
@buffer << draw_module_or_class_footer
|
84
|
-
@buffer.join("\n")
|
85
|
-
end
|
86
|
-
|
87
|
-
def draw_method(method_name, method_obj, expected_sig, is_class_method: false)
|
88
|
-
if !method_obj.present?
|
89
|
-
# not very actionable because this could be a method in a newer version of Rails
|
90
|
-
# puts "Skip method '#{method_name}' because there is no matching method object."
|
91
|
-
return
|
92
|
-
end
|
93
|
-
@buffer << generate_method_sig(method_name, expected_sig, is_class_method).indent(2)
|
94
|
-
end
|
95
|
-
|
96
|
-
def populate_activerecord_querying_methods
|
97
|
-
# All is a named scope that most method from ActiveRecord::Querying delegate to
|
98
|
-
# rails/activerecord/lib/active_record/querying.rb:21
|
99
|
-
@generated_scope_sigs["all"] = { ret: @model_relation_class_name }
|
100
|
-
@generated_scope_sigs["unscoped"] = {
|
101
|
-
ret: @model_relation_class_name,
|
102
|
-
args: [
|
103
|
-
{ name: :block, arg_type: :block, value_type: 'T.nilable(T.proc.void)' },
|
104
|
-
]
|
105
|
-
}
|
106
|
-
# It's not possible to typedef all methods in ActiveRecord::Querying module to have the
|
107
|
-
# matching type. By generating model-specific sig, we can typedef these methods to return
|
108
|
-
# <Model>::Relation class.
|
109
|
-
# rails/activerecord/lib/active_record/querying.rb
|
110
|
-
model_query_relation_methods = [
|
111
|
-
:select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
|
112
|
-
:where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
|
113
|
-
:having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
|
114
|
-
]
|
115
|
-
model_query_relation_methods.each do |method_name|
|
116
|
-
@generated_querying_sigs[method_name.to_s] = {
|
117
|
-
args: [
|
118
|
-
{name: :args, arg_type: :rest, value_type: 'T.untyped'},
|
119
|
-
{name: :block, arg_type: :block, value_type: 'T.nilable(T.proc.void)'},
|
120
|
-
],
|
121
|
-
ret: @model_relation_class_name,
|
122
|
-
}
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
def populate_named_scope_methods
|
127
|
-
@model_class.methods.sort.each do |method_name|
|
128
|
-
method_obj = @model_class.method(method_name)
|
129
|
-
next unless method_obj.present? && method_obj.source_location.present?
|
130
|
-
# we detect sscopes defined in a model by 2 criteria:
|
131
|
-
# - they don't have an owner name
|
132
|
-
# - they are defined in 'activerecord/lib/active_record/scoping/named.rb'
|
133
|
-
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/scoping/named.rb
|
134
|
-
next unless method_obj.owner.name == nil
|
135
|
-
source_file = method_obj.source_location[0]
|
136
|
-
next unless source_file.include?('lib/active_record/scoping/named.rb')
|
137
|
-
@generated_scope_sigs[method_name] = {
|
138
|
-
args: [ name: :args, arg_type: :rest, value_type: 'T.untyped' ],
|
139
|
-
ret: @model_relation_class_name,
|
140
|
-
}
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
def populate_generated_column_methods
|
145
|
-
@columns_hash.each do |column_name, column_def|
|
146
|
-
if @model_class.defined_enums.has_key?(column_name)
|
147
|
-
# enum attribute is treated differently
|
148
|
-
assignable_type = "T.any(Integer, String, Symbol)"
|
149
|
-
assignable_type = "T.nilable(#{assignable_type})" if column_def.null
|
150
|
-
@generated_instance_module_sigs.merge!({
|
151
|
-
"#{column_name}" => { ret: "String" },
|
152
|
-
"#{column_name}=" => {
|
153
|
-
args: [ name: :value, arg_type: :req, value_type: assignable_type],
|
154
|
-
},
|
155
|
-
})
|
156
|
-
else
|
157
|
-
column_type = type_for_column_def(column_def)
|
158
|
-
@generated_instance_module_sigs.merge!({
|
159
|
-
"#{column_name}" => { ret: column_type },
|
160
|
-
"#{column_name}=" => {
|
161
|
-
args: [ name: :value, arg_type: :req, value_type: column_type ],
|
162
|
-
},
|
163
|
-
})
|
164
|
-
end
|
165
|
-
|
166
|
-
@generated_instance_module_sigs["#{column_name}?"] = {
|
167
|
-
ret: "T::Boolean",
|
168
|
-
args: [ name: :args, arg_type: :rest, value_type: 'T.untyped' ],
|
169
|
-
}
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
def populate_generated_association_methods
|
174
|
-
@model_class.reflections.each do |assoc_name, reflection|
|
175
|
-
reflection.collection? ?
|
176
|
-
populate_collection_assoc_getter_setter(assoc_name, reflection) :
|
177
|
-
populate_single_assoc_getter_setter(assoc_name, reflection)
|
178
|
-
end
|
179
|
-
end
|
180
38
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
assoc_type = "T.nilable(#{assoc_class})"
|
185
|
-
if reflection.belongs_to?
|
186
|
-
# if this is a belongs_to connection, we may be able to detect whether
|
187
|
-
# this field is required & use a stronger type
|
188
|
-
column_def = @columns_hash[reflection.foreign_key.to_s]
|
189
|
-
if column_def
|
190
|
-
assoc_type = assoc_class if !column_def.null
|
191
|
-
end
|
39
|
+
# Collect the instances of each plugin into an array
|
40
|
+
plugin_instances = self.class.get_plugins.map do |plugin_klass|
|
41
|
+
plugin_klass.new(model_class, available_classes)
|
192
42
|
end
|
193
43
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
relation_class = relation_should_be_untyped?(reflection) ?
|
206
|
-
"ActiveRecord::Associations::CollectionProxy" :
|
207
|
-
"#{assoc_class}::ActiveRecord_Associations_CollectionProxy"
|
208
|
-
@generated_instance_sigs.merge!({
|
209
|
-
"#{assoc_name}" => { ret: relation_class },
|
210
|
-
"#{assoc_name}=" => {
|
211
|
-
args: [ name: :value, arg_type: :req, value_type: "T.any(T::Array[#{assoc_class}], #{relation_class})" ],
|
212
|
-
},
|
213
|
-
})
|
214
|
-
end
|
215
|
-
|
216
|
-
def populate_generated_enum_methods
|
217
|
-
@model_class.defined_enums.each do |enum_name, enum_hash|
|
218
|
-
@generated_class_sigs["#{enum_name.pluralize}"] = { ret: "T::Hash[T.any(String, Symbol), Integer]"}
|
219
|
-
enum_hash.keys.each do |enum_val|
|
220
|
-
@generated_instance_module_sigs["#{enum_val}?"] = { ret: "T::Boolean" }
|
221
|
-
@generated_instance_module_sigs["#{enum_val}!"] = { ret: nil }
|
222
|
-
@generated_scope_sigs["#{enum_val}"] = {
|
223
|
-
args: [ name: :args, arg_type: :rest, value_type: 'T.untyped' ],
|
224
|
-
ret: @model_relation_class_name,
|
225
|
-
}
|
226
|
-
# force generating these methods because sorbet's hidden-definitions generate & override them
|
227
|
-
@generated_class_sigs["#{enum_val}"] = {
|
228
|
-
args: [ name: :args, arg_type: :rest, value_type: 'T.untyped' ],
|
229
|
-
ret: @model_relation_class_name,
|
230
|
-
}
|
44
|
+
generator = Parlour::RbiGenerator.new(break_params: 3)
|
45
|
+
run_plugins(plugin_instances, generator, allow_failure: true)
|
46
|
+
# Generate the base after the plugins because when ConflictResolver merge the modules,
|
47
|
+
# it'll put the modules at the last position merged. Putting the base stuff
|
48
|
+
# last will keep the order consistent and minimize changes when new plugins are added.
|
49
|
+
generate_base_rbi(generator.root)
|
50
|
+
|
51
|
+
Parlour::ConflictResolver.new.resolve_conflicts(generator.root) do |msg, candidates|
|
52
|
+
puts "Conflict: #{msg}. Skip following methods"
|
53
|
+
candidates.each do |c|
|
54
|
+
puts "- Method `#{c.name}` generated by #{c.generated_by.class.name}"
|
231
55
|
end
|
56
|
+
nil
|
232
57
|
end
|
233
|
-
end
|
234
|
-
|
235
|
-
def assoc_should_be_untyped?(reflection)
|
236
|
-
polymorphic_assoc?(reflection) || !@available_classes.include?(reflection.klass.name)
|
237
|
-
end
|
238
|
-
|
239
|
-
def relation_should_be_untyped?(reflection)
|
240
|
-
# only type the relation we'll generate
|
241
|
-
assoc_should_be_untyped?(reflection) || !@available_classes.include?(reflection.klass.name)
|
242
|
-
end
|
243
58
|
|
244
|
-
def polymorphic_assoc?(reflection)
|
245
|
-
reflection.through_reflection ?
|
246
|
-
polymorphic_assoc?(reflection.source_reflection) :
|
247
|
-
reflection.polymorphic?
|
248
|
-
end
|
249
|
-
|
250
|
-
def draw_file_header_and_base_classes
|
251
|
-
# We define a custom <ModelName>::Relation class so that it can be extended
|
252
|
-
# to contain custom scopes for each models
|
253
|
-
<<~MESSAGE
|
254
|
-
# This is an autogenerated file for dynamic methods in #{@model_class.name}
|
255
|
-
# Please rerun rake rails_rbi:models to regenerate.
|
256
|
-
# typed: strong
|
257
|
-
|
258
|
-
class #{@model_relation_class_name} < ActiveRecord::Relation
|
259
|
-
include #{@model_class.name}::#{MODEL_RELATION_SHARED_MODULE_SUFFIX}
|
260
|
-
extend T::Generic
|
261
|
-
Elem = type_member(fixed: #{@model_class.name})
|
262
|
-
end
|
263
|
-
|
264
|
-
class #{@model_class.name}::ActiveRecord_Associations_CollectionProxy < ActiveRecord::Associations::CollectionProxy
|
265
|
-
include #{@model_class.name}::#{MODEL_RELATION_SHARED_MODULE_SUFFIX}
|
266
|
-
extend T::Generic
|
267
|
-
Elem = type_member(fixed: #{@model_class.name})
|
268
|
-
end
|
269
|
-
|
270
|
-
class #{@model_class.name} < #{@model_class.superclass}
|
271
|
-
extend T::Sig
|
272
|
-
extend T::Generic
|
273
|
-
extend #{@model_class.name}::#{MODEL_RELATION_SHARED_MODULE_SUFFIX}
|
274
|
-
include #{@model_class.name}::#{MODEL_INSTANCE_MODULE_SUFFIX}
|
275
|
-
Elem = type_template(fixed: #{@model_class.name})
|
276
|
-
end
|
277
|
-
MESSAGE
|
278
|
-
end
|
279
|
-
|
280
|
-
def draw_module_or_class_footer
|
281
59
|
<<~MESSAGE
|
282
|
-
|
283
|
-
|
284
|
-
end
|
60
|
+
# This is an autogenerated file for dynamic methods in #{self.model_class_name}
|
61
|
+
# Please rerun rake rails_rbi:models[#{self.model_class_name}] to regenerate.
|
285
62
|
|
286
|
-
|
287
|
-
<<~MESSAGE
|
288
|
-
module #{name}
|
289
|
-
extend T::Sig
|
63
|
+
#{generator.rbi}
|
290
64
|
MESSAGE
|
291
65
|
end
|
292
66
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
Integer
|
329
|
-
when ActiveRecord::Type::Binary, ActiveRecord::Type::String, ActiveRecord::Type::Text
|
330
|
-
String
|
331
|
-
else
|
332
|
-
# Json type is only supported in Rails 5.2 and above
|
333
|
-
case
|
334
|
-
when Object.const_defined?('ActiveRecord::Type::Json') && klass.is_a?(ActiveRecord::Type::Json)
|
335
|
-
"T.any(Array, T::Boolean, Float, Hash, Integer, String)"
|
336
|
-
when Object.const_defined?('ActiveRecord::Enum::EnumType') && klass.is_a?(ActiveRecord::Enum::EnumType)
|
337
|
-
String
|
338
|
-
else
|
339
|
-
"T.untyped"
|
340
|
-
end
|
341
|
-
end
|
342
|
-
end
|
67
|
+
sig { params(root: Parlour::RbiGenerator::Namespace).void }
|
68
|
+
def generate_base_rbi(root)
|
69
|
+
# This is the backbone of the model_rbi_formatter.
|
70
|
+
# It could live in a base plugin but I consider it not replacable and better to leave here
|
71
|
+
model_relation_rbi = root.create_class(
|
72
|
+
self.model_relation_class_name,
|
73
|
+
superclass: "ActiveRecord::Relation",
|
74
|
+
)
|
75
|
+
model_relation_rbi.create_include(self.model_relation_shared_module_name)
|
76
|
+
model_relation_rbi.create_extend("T::Sig")
|
77
|
+
model_relation_rbi.create_extend("T::Generic")
|
78
|
+
model_relation_rbi.create_constant(
|
79
|
+
"Elem",
|
80
|
+
value: "type_member(fixed: #{model_class_name})",
|
81
|
+
)
|
82
|
+
|
83
|
+
collection_proxy_rbi = root.create_class(
|
84
|
+
self.model_assoc_proxy_class_name,
|
85
|
+
superclass: "ActiveRecord::Associations::CollectionProxy",
|
86
|
+
)
|
87
|
+
collection_proxy_rbi.create_include(self.model_relation_shared_module_name)
|
88
|
+
collection_proxy_rbi.create_extend("T::Sig")
|
89
|
+
collection_proxy_rbi.create_extend("T::Generic")
|
90
|
+
collection_proxy_rbi.create_constant(
|
91
|
+
"Elem",
|
92
|
+
value: "type_member(fixed: #{self.model_class_name})",
|
93
|
+
)
|
94
|
+
|
95
|
+
model_rbi = root.create_class(
|
96
|
+
self.model_class_name,
|
97
|
+
superclass: T.must(@model_class.superclass).name,
|
98
|
+
)
|
99
|
+
model_rbi.create_extend("T::Sig")
|
100
|
+
model_rbi.create_extend("T::Generic")
|
101
|
+
model_rbi.create_extend(self.model_relation_shared_module_name)
|
343
102
|
|
344
|
-
|
345
|
-
#
|
346
|
-
#
|
347
|
-
# . ret: <return_type>
|
348
|
-
# args: [ name: :value, arg_type: :req, value_type: "T.any(T::Array[#{assoc_class}], ActiveRecord::Relation" ]
|
349
|
-
# }
|
350
|
-
#
|
351
|
-
# Generate something like this
|
103
|
+
# <Model>::MODEL_RELATION_SHARED_MODULE_SUFFIX is a fake module added so that
|
104
|
+
# when a method is defined in this module, it'll be added to both the Model class
|
105
|
+
# as a class method and to its relation as an instance method.
|
352
106
|
#
|
353
|
-
#
|
354
|
-
#
|
355
|
-
#
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
107
|
+
# We need to define the module after the other classes
|
108
|
+
# to work around Sorbet loading order bug
|
109
|
+
# https://sorbet-ruby.slack.com/archives/CHN2L03NH/p1556065791047300
|
110
|
+
model_relation_shared_rbi = root.create_module(self.model_relation_shared_module_name)
|
111
|
+
model_relation_shared_rbi.create_extend("T::Sig")
|
112
|
+
end
|
113
|
+
|
114
|
+
sig {
|
115
|
+
params(
|
116
|
+
plugins: T::Array[Parlour::Plugin],
|
117
|
+
generator: Parlour::RbiGenerator,
|
118
|
+
allow_failure: T::Boolean,
|
119
|
+
).
|
120
|
+
void
|
121
|
+
}
|
122
|
+
def run_plugins(plugins, generator, allow_failure: true)
|
123
|
+
plugins.each do |plugin|
|
124
|
+
begin
|
125
|
+
generator.current_plugin = plugin
|
126
|
+
plugin.generate(generator.root)
|
127
|
+
rescue Exception => e
|
128
|
+
raise e unless allow_failure
|
129
|
+
puts "!!! Plugin #{plugin.class.name} threw an exception: #{e}"
|
130
|
+
end
|
374
131
|
end
|
375
|
-
|
376
|
-
method_prefix = is_class_method ? 'self.' : ''
|
377
|
-
|
378
|
-
return_type = generated_method_def[:ret] ?
|
379
|
-
"returns(#{generated_method_def[:ret]})" :
|
380
|
-
"void"
|
381
|
-
|
382
|
-
<<~MESSAGE
|
383
|
-
sig { #{param_sig}#{return_type} }
|
384
|
-
def #{method_prefix}#{method_name}(#{param_def}); end
|
385
|
-
MESSAGE
|
386
132
|
end
|
387
133
|
end
|