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.
Files changed (171) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +13 -1
  3. data/.travis.yml +2 -2
  4. data/Gemfile +1 -1
  5. data/README.md +79 -3
  6. data/lib/sorbet-rails.rb +5 -1
  7. data/lib/sorbet-rails/activerecord.rbi +27 -0
  8. data/lib/sorbet-rails/custom_finder_methods.rb +11 -0
  9. data/lib/sorbet-rails/helper_rbi_formatter.rb +33 -0
  10. data/lib/sorbet-rails/model_plugins/active_record_assoc.rb +91 -0
  11. data/lib/sorbet-rails/model_plugins/active_record_attribute.rb +111 -0
  12. data/lib/sorbet-rails/model_plugins/active_record_enum.rb +43 -0
  13. data/lib/sorbet-rails/model_plugins/active_record_finder_methods.rb +80 -0
  14. data/lib/sorbet-rails/model_plugins/active_record_named_scope.rb +28 -0
  15. data/lib/sorbet-rails/model_plugins/active_record_querying.rb +42 -0
  16. data/lib/sorbet-rails/model_plugins/active_relation_where_not.rb +28 -0
  17. data/lib/sorbet-rails/model_plugins/base.rb +33 -0
  18. data/lib/sorbet-rails/model_plugins/custom_finder_methods.rb +54 -0
  19. data/lib/sorbet-rails/model_plugins/enumerable_collections.rb +49 -0
  20. data/lib/sorbet-rails/model_plugins/plugins.rb +45 -0
  21. data/lib/sorbet-rails/model_rbi_formatter.rb +108 -362
  22. data/lib/sorbet-rails/model_utils.rb +45 -0
  23. data/lib/sorbet-rails/railtie.rb +1 -0
  24. data/lib/sorbet-rails/routes_rbi_formatter.rb +12 -3
  25. data/lib/sorbet-rails/tasks/rails_rbi.rake +36 -15
  26. data/lib/sorbet-rails/utils.rb +4 -0
  27. data/sorbet-rails.gemspec +4 -2
  28. data/spec/helper_rbi_formatter_spec.rb +13 -0
  29. data/spec/model_plugins_spec.rb +31 -0
  30. data/spec/model_rbi_formatter_spec.rb +5 -5
  31. data/spec/rake_rails_rbi_helpers_spec.rb +14 -0
  32. data/spec/routes_rbi_formatter_spec.rb +3 -3
  33. data/spec/sorbet_spec.rb +12 -16
  34. data/spec/support/rails_shared/app/controllers/application_controller.rb +1 -1
  35. data/spec/support/rails_shared/app/helpers/bar_helper.rb +2 -0
  36. data/spec/support/rails_shared/app/helpers/baz_helper.rb +2 -0
  37. data/spec/support/rails_shared/app/helpers/foo_helper.rb +2 -0
  38. data/spec/support/rails_shared/app/models/concerns/mythical.rb +10 -0
  39. data/spec/support/rails_shared/app/models/wand.rb +1 -0
  40. data/spec/support/rails_shared/config/initializers/sorbet_rails.rb +3 -0
  41. data/spec/support/rails_shared/lib/mythical_rbi_plugin.rb +16 -0
  42. data/spec/support/rails_shared/sorbet_test_cases.rb +5 -2
  43. data/spec/support/rails_shared/typed-override.yaml +2 -0
  44. data/spec/support/rails_symlinks/app/helpers +1 -0
  45. data/spec/support/rails_symlinks/config/initializers/sorbet_rails.rb +1 -0
  46. data/spec/support/rails_symlinks/lib/mythical_rbi_plugin.rb +1 -0
  47. data/spec/support/rails_symlinks/typed-override.yaml +1 -0
  48. data/spec/support/v4.2/Gemfile.lock +5 -0
  49. data/spec/support/v4.2/app/helpers +1 -0
  50. data/spec/support/v4.2/config/initializers/sorbet_rails.rb +1 -0
  51. data/spec/support/v4.2/config/routes.rb +0 -1
  52. data/spec/support/v4.2/lib/mythical_rbi_plugin.rb +1 -0
  53. data/spec/support/v4.2/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
  54. data/spec/support/v4.2/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
  55. data/spec/support/v4.2/typed-override.yaml +1 -0
  56. data/spec/support/v5.0/Gemfile.lock +5 -0
  57. data/spec/support/v5.0/app/helpers +1 -0
  58. data/spec/support/v5.0/config/initializers/sorbet_rails.rb +1 -0
  59. data/spec/support/v5.0/config/routes.rb +0 -1
  60. data/spec/support/v5.0/lib/mythical_rbi_plugin.rb +1 -0
  61. data/spec/support/v5.0/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
  62. data/spec/support/v5.0/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
  63. data/spec/support/v5.0/typed-override.yaml +1 -0
  64. data/spec/support/v5.1/Gemfile.lock +5 -0
  65. data/spec/support/v5.1/app/helpers +1 -0
  66. data/spec/support/v5.1/config/initializers/sorbet_rails.rb +1 -0
  67. data/spec/support/v5.1/lib/mythical_rbi_plugin.rb +1 -0
  68. data/spec/support/v5.1/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
  69. data/spec/support/v5.1/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
  70. data/spec/support/v5.1/typed-override.yaml +1 -0
  71. data/spec/support/v5.2-no-sorbet/Gemfile +0 -2
  72. data/spec/support/v5.2-no-sorbet/Gemfile.lock +7 -5
  73. data/spec/support/v5.2-no-sorbet/app/helpers +1 -0
  74. data/spec/support/v5.2-no-sorbet/config/initializers/sorbet_rails.rb +1 -0
  75. data/spec/support/v5.2-no-sorbet/lib/mythical_rbi_plugin.rb +1 -0
  76. data/spec/support/v5.2-no-sorbet/sorbet_test_cases.rb +1 -0
  77. data/spec/support/v5.2-no-sorbet/typed-override.yaml +1 -0
  78. data/spec/support/v5.2/Gemfile +0 -2
  79. data/spec/support/v5.2/Gemfile.lock +7 -5
  80. data/spec/support/v5.2/app/helpers +1 -0
  81. data/spec/support/v5.2/config/initializers/sorbet_rails.rb +1 -0
  82. data/spec/support/v5.2/lib/mythical_rbi_plugin.rb +1 -0
  83. data/spec/support/v5.2/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
  84. data/spec/support/v5.2/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
  85. data/spec/support/v5.2/typed-override.yaml +1 -0
  86. data/spec/support/v6.0/Gemfile +1 -3
  87. data/spec/support/v6.0/Gemfile.lock +61 -59
  88. data/spec/support/v6.0/app/helpers +1 -0
  89. data/spec/support/v6.0/config/initializers/sorbet_rails.rb +1 -0
  90. data/spec/support/v6.0/config/routes.rb +0 -1
  91. data/spec/support/v6.0/lib/mythical_rbi_plugin.rb +1 -0
  92. data/spec/support/v6.0/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
  93. data/spec/support/v6.0/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
  94. data/spec/support/v6.0/typed-override.yaml +1 -0
  95. data/spec/test_data/v4.2/expected_helpers.rbi +17 -0
  96. data/spec/test_data/v4.2/expected_potion.rbi +259 -28
  97. data/spec/test_data/v4.2/expected_spell_book.rbi +278 -37
  98. data/spec/test_data/v4.2/expected_srb_tc_output.txt +1 -99
  99. data/spec/test_data/v4.2/expected_wand.rbi +345 -96
  100. data/spec/test_data/v4.2/expected_wizard.rbi +322 -76
  101. data/spec/test_data/v4.2/expected_wizard_wo_spellbook.rbi +322 -76
  102. data/spec/test_data/v5.0/expected_helpers.rbi +17 -0
  103. data/spec/test_data/v5.0/expected_internal_metadata.rbi +273 -37
  104. data/spec/test_data/v5.0/expected_potion.rbi +259 -28
  105. data/spec/test_data/v5.0/expected_schema_migration.rbi +264 -28
  106. data/spec/test_data/v5.0/expected_spell_book.rbi +278 -37
  107. data/spec/test_data/v5.0/expected_srb_tc_output.txt +1 -81
  108. data/spec/test_data/v5.0/expected_wand.rbi +341 -92
  109. data/spec/test_data/v5.0/expected_wizard.rbi +318 -72
  110. data/spec/test_data/v5.0/expected_wizard_wo_spellbook.rbi +318 -72
  111. data/spec/test_data/v5.1/expected_helpers.rbi +17 -0
  112. data/spec/test_data/v5.1/expected_internal_metadata.rbi +273 -37
  113. data/spec/test_data/v5.1/expected_potion.rbi +259 -28
  114. data/spec/test_data/v5.1/expected_schema_migration.rbi +264 -28
  115. data/spec/test_data/v5.1/expected_spell_book.rbi +278 -37
  116. data/spec/test_data/v5.1/expected_srb_tc_output.txt +1 -81
  117. data/spec/test_data/v5.1/expected_wand.rbi +341 -92
  118. data/spec/test_data/v5.1/expected_wizard.rbi +318 -72
  119. data/spec/test_data/v5.1/expected_wizard_wo_spellbook.rbi +318 -72
  120. data/spec/test_data/v5.2-no-sorbet/expected_attachment.rbi +265 -29
  121. data/spec/test_data/v5.2-no-sorbet/expected_blob.rbi +271 -35
  122. data/spec/test_data/v5.2-no-sorbet/expected_helpers.rbi +17 -0
  123. data/spec/test_data/v5.2-no-sorbet/expected_internal_metadata.rbi +273 -37
  124. data/spec/test_data/v5.2-no-sorbet/expected_potion.rbi +259 -28
  125. data/spec/test_data/v5.2-no-sorbet/expected_schema_migration.rbi +264 -28
  126. data/spec/test_data/v5.2-no-sorbet/expected_spell_book.rbi +278 -37
  127. data/spec/test_data/v5.2-no-sorbet/expected_srb_tc_output.txt +1 -81
  128. data/spec/test_data/v5.2-no-sorbet/expected_wand.rbi +351 -102
  129. data/spec/test_data/v5.2-no-sorbet/expected_wizard.rbi +318 -72
  130. data/spec/test_data/v5.2-no-sorbet/expected_wizard_wo_spellbook.rbi +318 -72
  131. data/spec/test_data/v5.2/expected_attachment.rbi +265 -29
  132. data/spec/test_data/v5.2/expected_blob.rbi +271 -35
  133. data/spec/test_data/v5.2/expected_helpers.rbi +17 -0
  134. data/spec/test_data/v5.2/expected_internal_metadata.rbi +273 -37
  135. data/spec/test_data/v5.2/expected_potion.rbi +259 -28
  136. data/spec/test_data/v5.2/expected_schema_migration.rbi +264 -28
  137. data/spec/test_data/v5.2/expected_spell_book.rbi +278 -37
  138. data/spec/test_data/v5.2/expected_srb_tc_output.txt +1 -81
  139. data/spec/test_data/v5.2/expected_wand.rbi +351 -102
  140. data/spec/test_data/v5.2/expected_wizard.rbi +318 -72
  141. data/spec/test_data/v5.2/expected_wizard_wo_spellbook.rbi +318 -72
  142. data/spec/test_data/v6.0/expected_attachment.rbi +265 -29
  143. data/spec/test_data/v6.0/expected_blob.rbi +271 -35
  144. data/spec/test_data/v6.0/expected_helpers.rbi +17 -0
  145. data/spec/test_data/v6.0/expected_internal_metadata.rbi +273 -37
  146. data/spec/test_data/v6.0/expected_potion.rbi +259 -28
  147. data/spec/test_data/v6.0/expected_schema_migration.rbi +264 -28
  148. data/spec/test_data/v6.0/expected_spell_book.rbi +278 -37
  149. data/spec/test_data/v6.0/expected_srb_tc_output.txt +1 -81
  150. data/spec/test_data/v6.0/expected_wand.rbi +351 -102
  151. data/spec/test_data/v6.0/expected_wizard.rbi +318 -72
  152. data/spec/test_data/v6.0/expected_wizard_wo_spellbook.rbi +318 -72
  153. metadata +120 -37
  154. data/lib/sorbet-rails/rbi/activerecord.rbi +0 -207
  155. data/spec/support/v4.2/app/helpers/application_helper.rb +0 -3
  156. data/spec/support/v4.2/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
  157. data/spec/support/v4.2/sorbet/rbi/hidden-definitions/errors.txt +0 -11998
  158. data/spec/support/v4.2/sorbet/rbi/hidden-definitions/hidden.rbi +0 -27774
  159. data/spec/support/v5.0/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
  160. data/spec/support/v5.0/sorbet/rbi/hidden-definitions/errors.txt +0 -10523
  161. data/spec/support/v5.0/sorbet/rbi/hidden-definitions/hidden.rbi +0 -24969
  162. data/spec/support/v5.1/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
  163. data/spec/support/v5.1/sorbet/rbi/hidden-definitions/errors.txt +0 -10226
  164. data/spec/support/v5.1/sorbet/rbi/hidden-definitions/hidden.rbi +0 -24635
  165. data/spec/support/v5.2/.ruby-version +0 -1
  166. data/spec/support/v5.2/sorbet/rbi/gems/sorbet-runtime.rbi +0 -644
  167. data/spec/support/v5.2/sorbet/rbi/hidden-definitions/errors.txt +0 -10046
  168. data/spec/support/v5.2/sorbet/rbi/hidden-definitions/hidden.rbi +0 -24424
  169. data/spec/support/v6.0/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
  170. data/spec/support/v6.0/sorbet/rbi/hidden-definitions/errors.txt +0 -12074
  171. 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: true
2
- class ModelRbiFormatter
3
- MODEL_RELATION_SHARED_MODULE_SUFFIX = "ModelRelationShared"
4
- MODEL_CLASS_MODULE_SUFFIX = "ClassMethods"
5
- MODEL_INSTANCE_MODULE_SUFFIX = "InstanceMethods"
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
- def populate_single_assoc_getter_setter(assoc_name, reflection)
182
- # TODO allow people to specify the possible values of polymorphic associations
183
- assoc_class = assoc_should_be_untyped?(reflection) ? "T.untyped" : "::#{reflection.klass.name}"
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
- @generated_instance_sigs.merge!({
195
- "#{assoc_name}" => { ret: assoc_type },
196
- "#{assoc_name}=" => {
197
- args: [ name: :value, arg_type: :req, value_type: assoc_type ],
198
- },
199
- })
200
- end
201
-
202
- def populate_collection_assoc_getter_setter(assoc_name, reflection)
203
- # TODO allow people to specify the possible values of polymorphic associations
204
- assoc_class = assoc_should_be_untyped?(reflection) ? "T.untyped" : "::#{reflection.klass.name}"
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
- end
283
- MESSAGE
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
- def draw_module_header(name)
287
- <<~MESSAGE
288
- module #{name}
289
- extend T::Sig
63
+ #{generator.rbi}
290
64
  MESSAGE
291
65
  end
292
66
 
293
- def draw_class_header(name)
294
- <<~MESSAGE
295
- class #{name}
296
- extend T::Sig
297
- MESSAGE
298
- end
299
-
300
- def type_for_column_def(column_def)
301
- cast_type = ActiveRecord::Base.connection.respond_to?(:lookup_cast_type_from_column) ?
302
- ActiveRecord::Base.connection.lookup_cast_type_from_column(column_def) :
303
- column_def.cast_type
304
-
305
- strict_type = active_record_type_to_sorbet_type(cast_type)
306
-
307
- if column_def.respond_to?(:array?) && column_def.array?
308
- strict_type = "T::Array[#{strict_type}]"
309
- end
310
- column_def.null ? "T.nilable(#{strict_type})" : strict_type
311
- end
312
-
313
- def active_record_type_to_sorbet_type(klass)
314
- case klass
315
- when ActiveRecord::Type::Boolean
316
- "T::Boolean"
317
- when ActiveRecord::Type::DateTime
318
- DateTime
319
- when ActiveRecord::Type::Date
320
- Date
321
- when ActiveRecord::Type::Decimal
322
- BigDecimal
323
- when ActiveRecord::Type::Float
324
- Float
325
- when ActiveRecord::Type::Time
326
- Time
327
- when ActiveRecord::Type::BigInteger, ActiveRecord::Type::Integer, ActiveRecord::Type::DecimalWithoutScale, ActiveRecord::Type::UnsignedInteger
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
- def generate_method_sig(method_name, generated_method_def, is_class_method)
345
- # generated_method_def:
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
- # sig {returns(T.nilable(String))}
354
- # .def email; end
355
- # sig {params(record: T.nilable(String)).void}
356
- # def email=(record); end
357
-
358
- param_sig = ""
359
- param_def = ""
360
- if generated_method_def[:args]
361
- sig_args_string = generated_method_def[:args].map { |arg_def|
362
- "#{arg_def[:name]}: #{arg_def[:value_type]}"
363
- }.join(", ")
364
- param_sig = "params(#{sig_args_string})."
365
-
366
- param_def = generated_method_def[:args].map { |arg_def|
367
- prefix = ""
368
- prefix = "*" if arg_def[:arg_type] == :rest
369
- prefix = "**" if arg_def[:arg_type] == :keyrest
370
- prefix = "&" if arg_def[:arg_type] == :block
371
-
372
- "#{prefix}#{arg_def[:name]}"
373
- }.join(", ")
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