sorbet-rails 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (204) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/README.md +3 -3
  4. data/lib/bundled_rbi/typed_enum.rbi +7 -0
  5. data/lib/sorbet-rails.rb +0 -2
  6. data/lib/sorbet-rails/active_record_rbi_formatter.rb +301 -0
  7. data/lib/sorbet-rails/config.rb +0 -1
  8. data/lib/sorbet-rails/dependent_gem_rbis/activerecord.rbi +11 -0
  9. data/lib/sorbet-rails/deprecation.rb +5 -0
  10. data/lib/sorbet-rails/gem_plugins/active_flag_plugin.rb +0 -1
  11. data/lib/sorbet-rails/gem_plugins/paperclip_plugin.rb +0 -1
  12. data/lib/sorbet-rails/model_plugins/active_record_assoc.rb +36 -8
  13. data/lib/sorbet-rails/model_plugins/active_record_attribute.rb +20 -9
  14. data/lib/sorbet-rails/model_plugins/active_record_enum.rb +0 -1
  15. data/lib/sorbet-rails/model_plugins/active_record_named_scope.rb +15 -6
  16. data/lib/sorbet-rails/model_plugins/active_record_querying.rb +34 -3
  17. data/lib/sorbet-rails/model_plugins/active_storage_methods.rb +1 -1
  18. data/lib/sorbet-rails/model_plugins/base.rb +10 -0
  19. data/lib/sorbet-rails/model_plugins/enumerable_collections.rb +0 -50
  20. data/lib/sorbet-rails/model_plugins/plugins.rb +0 -3
  21. data/lib/sorbet-rails/model_rbi_formatter.rb +6 -10
  22. data/lib/sorbet-rails/model_utils.rb +83 -36
  23. data/lib/sorbet-rails/rails_mixins/generated_url_helpers.rb +2 -3
  24. data/lib/sorbet-rails/railtie.rb +0 -2
  25. data/lib/sorbet-rails/tasks/rails_rbi.rake +32 -24
  26. data/sorbet-rails.gemspec +2 -2
  27. data/spec/active_record_rbi_formatter_spec.rb +24 -0
  28. data/spec/generators/rails-template.rb +90 -6
  29. data/spec/generators/sorbet_test_cases.rb +204 -122
  30. data/spec/model_rbi_formatter_spec.rb +1 -1
  31. data/spec/rails_helper.rb +14 -1
  32. data/spec/rake_rails_rbi_active_record_spec.rb +21 -0
  33. data/spec/rake_rails_rbi_models_spec.rb +7 -0
  34. data/spec/sorbet_spec.rb +12 -1
  35. data/spec/support/v5.0/Gemfile.lock +8 -8
  36. data/spec/support/v5.0/app/models/headmaster.rb +8 -0
  37. data/spec/support/v5.0/app/models/school.rb +2 -0
  38. data/spec/support/v5.0/app/models/spell.rb +5 -0
  39. data/spec/support/v5.0/app/models/spell_book.rb +5 -0
  40. data/spec/support/v5.0/app/models/subject.rb +5 -0
  41. data/spec/support/v5.0/app/models/wizard.rb +6 -1
  42. data/spec/support/v5.0/db/migrate/20190620000010_add_subject.rb +8 -0
  43. data/spec/support/v5.0/db/migrate/20190620000011_add_subjects_wizards.rb +8 -0
  44. data/spec/support/v5.0/db/migrate/20190620000012_add_spell.rb +8 -0
  45. data/spec/support/v5.0/db/migrate/20190620000013_add_spells_spell_books.rb +8 -0
  46. data/spec/support/v5.0/db/migrate/20190620000014_create_headmasters.rb +9 -0
  47. data/spec/support/v5.0/db/schema.rb +28 -1
  48. data/spec/support/v5.0/lib/mythical_rbi_plugin.rb +1 -1
  49. data/spec/support/v5.0/sorbet_test_cases.rb +204 -122
  50. data/spec/support/v5.1/Gemfile.lock +8 -8
  51. data/spec/support/v5.1/app/models/headmaster.rb +8 -0
  52. data/spec/support/v5.1/app/models/school.rb +2 -0
  53. data/spec/support/v5.1/app/models/spell.rb +5 -0
  54. data/spec/support/v5.1/app/models/spell_book.rb +5 -0
  55. data/spec/support/v5.1/app/models/subject.rb +5 -0
  56. data/spec/support/v5.1/app/models/wizard.rb +6 -1
  57. data/spec/support/v5.1/db/migrate/20190620000010_add_subject.rb +8 -0
  58. data/spec/support/v5.1/db/migrate/20190620000011_add_subjects_wizards.rb +8 -0
  59. data/spec/support/v5.1/db/migrate/20190620000012_add_spell.rb +8 -0
  60. data/spec/support/v5.1/db/migrate/20190620000013_add_spells_spell_books.rb +8 -0
  61. data/spec/support/v5.1/db/migrate/20190620000014_create_headmasters.rb +9 -0
  62. data/spec/support/v5.1/db/schema.rb +28 -1
  63. data/spec/support/v5.1/lib/mythical_rbi_plugin.rb +1 -1
  64. data/spec/support/v5.1/sorbet_test_cases.rb +204 -122
  65. data/spec/support/v5.2/Gemfile +1 -1
  66. data/spec/support/v5.2/Gemfile.lock +9 -9
  67. data/spec/support/v5.2/app/models/headmaster.rb +8 -0
  68. data/spec/support/v5.2/app/models/school.rb +2 -0
  69. data/spec/support/v5.2/app/models/spell.rb +5 -0
  70. data/spec/support/v5.2/app/models/spell_book.rb +5 -0
  71. data/spec/support/v5.2/app/models/subject.rb +5 -0
  72. data/spec/support/v5.2/app/models/wizard.rb +5 -0
  73. data/spec/support/v5.2/config/puma.rb +3 -0
  74. data/spec/support/v5.2/db/migrate/20190620000010_add_subject.rb +8 -0
  75. data/spec/support/v5.2/db/migrate/20190620000011_add_subjects_wizards.rb +8 -0
  76. data/spec/support/v5.2/db/migrate/20190620000012_add_spell.rb +8 -0
  77. data/spec/support/v5.2/db/migrate/20190620000013_add_spells_spell_books.rb +8 -0
  78. data/spec/support/v5.2/db/migrate/20190620000014_create_headmasters.rb +9 -0
  79. data/spec/support/v5.2/db/schema.rb +28 -1
  80. data/spec/support/v5.2/lib/mythical_rbi_plugin.rb +1 -1
  81. data/spec/support/v5.2/sorbet_test_cases.rb +204 -122
  82. data/spec/support/v6.0/Gemfile.lock +8 -8
  83. data/spec/support/v6.0/app/models/headmaster.rb +8 -0
  84. data/spec/support/v6.0/app/models/school.rb +2 -0
  85. data/spec/support/v6.0/app/models/spell.rb +5 -0
  86. data/spec/support/v6.0/app/models/spell_book.rb +5 -0
  87. data/spec/support/v6.0/app/models/subject.rb +5 -0
  88. data/spec/support/v6.0/app/models/wizard.rb +6 -1
  89. data/spec/support/v6.0/db/migrate/20190620000010_add_subject.rb +8 -0
  90. data/spec/support/v6.0/db/migrate/20190620000011_add_subjects_wizards.rb +8 -0
  91. data/spec/support/v6.0/db/migrate/20190620000012_add_spell.rb +8 -0
  92. data/spec/support/v6.0/db/migrate/20190620000013_add_spells_spell_books.rb +8 -0
  93. data/spec/support/v6.0/db/migrate/20190620000014_create_headmasters.rb +9 -0
  94. data/spec/support/v6.0/db/schema.rb +28 -1
  95. data/spec/support/v6.0/lib/mythical_rbi_plugin.rb +1 -1
  96. data/spec/support/v6.0/sorbet_test_cases.rb +204 -122
  97. data/spec/test_data/v5.0/expected_active_record_base.rbi +113 -0
  98. data/spec/test_data/v5.0/expected_active_record_relation.rbi +199 -0
  99. data/spec/test_data/v5.0/expected_habtm_subjects.rbi +660 -0
  100. data/spec/test_data/v5.0/expected_habtm_wizards.rbi +660 -0
  101. data/spec/test_data/v5.0/expected_headmaster.rbi +304 -0
  102. data/spec/test_data/v5.0/expected_internal_metadata.rbi +38 -401
  103. data/spec/test_data/v5.0/expected_potion.rbi +38 -401
  104. data/spec/test_data/v5.0/expected_robe.rbi +38 -403
  105. data/spec/test_data/v5.0/expected_schema_migration.rbi +38 -401
  106. data/spec/test_data/v5.0/expected_school.rbi +47 -401
  107. data/spec/test_data/v5.0/expected_spell.rbi +292 -0
  108. data/spec/test_data/v5.0/expected_spell/habtm_spell_books.rbi +295 -0
  109. data/spec/test_data/v5.0/expected_spell_book.rbi +86 -432
  110. data/spec/test_data/v5.0/expected_spell_book/habtm_spell_books.rbi +637 -0
  111. data/spec/test_data/v5.0/expected_spell_book/habtm_spells.rbi +295 -0
  112. data/spec/test_data/v5.0/expected_squib.rbi +122 -477
  113. data/spec/test_data/v5.0/expected_subject.rbi +292 -0
  114. data/spec/test_data/v5.0/expected_subject/habtm_wizards.rbi +295 -0
  115. data/spec/test_data/v5.0/expected_wand.rbi +75 -442
  116. data/spec/test_data/v5.0/expected_wizard.rbi +144 -498
  117. data/spec/test_data/v5.0/expected_wizard/habtm_subjects.rbi +295 -0
  118. data/spec/test_data/v5.0/expected_wizard_wo_spellbook.rbi +138 -498
  119. data/spec/test_data/v5.1/expected_active_record_base.rbi +113 -0
  120. data/spec/test_data/v5.1/expected_active_record_relation.rbi +178 -0
  121. data/spec/test_data/v5.1/expected_habtm_subjects.rbi +672 -0
  122. data/spec/test_data/v5.1/expected_habtm_wizards.rbi +672 -0
  123. data/spec/test_data/v5.1/expected_headmaster.rbi +310 -0
  124. data/spec/test_data/v5.1/expected_internal_metadata.rbi +38 -407
  125. data/spec/test_data/v5.1/expected_potion.rbi +38 -407
  126. data/spec/test_data/v5.1/expected_robe.rbi +38 -409
  127. data/spec/test_data/v5.1/expected_schema_migration.rbi +38 -407
  128. data/spec/test_data/v5.1/expected_school.rbi +47 -407
  129. data/spec/test_data/v5.1/expected_spell.rbi +298 -0
  130. data/spec/test_data/v5.1/expected_spell/habtm_spell_books.rbi +301 -0
  131. data/spec/test_data/v5.1/expected_spell_book.rbi +86 -438
  132. data/spec/test_data/v5.1/expected_spell_book/habtm_spell_books.rbi +649 -0
  133. data/spec/test_data/v5.1/expected_spell_book/habtm_spells.rbi +301 -0
  134. data/spec/test_data/v5.1/expected_squib.rbi +123 -484
  135. data/spec/test_data/v5.1/expected_subject.rbi +298 -0
  136. data/spec/test_data/v5.1/expected_subject/habtm_wizards.rbi +301 -0
  137. data/spec/test_data/v5.1/expected_wand.rbi +75 -448
  138. data/spec/test_data/v5.1/expected_wizard.rbi +145 -505
  139. data/spec/test_data/v5.1/expected_wizard/habtm_subjects.rbi +301 -0
  140. data/spec/test_data/v5.1/expected_wizard_wo_spellbook.rbi +139 -505
  141. data/spec/test_data/v5.2/expected_active_record_base.rbi +113 -0
  142. data/spec/test_data/v5.2/expected_active_record_relation.rbi +175 -0
  143. data/spec/test_data/v5.2/expected_attachment.rbi +38 -407
  144. data/spec/test_data/v5.2/expected_blob.rbi +60 -426
  145. data/spec/test_data/v5.2/expected_habtm_subjects.rbi +672 -0
  146. data/spec/test_data/v5.2/expected_habtm_wizards.rbi +672 -0
  147. data/spec/test_data/v5.2/expected_headmaster.rbi +310 -0
  148. data/spec/test_data/v5.2/expected_internal_metadata.rbi +38 -407
  149. data/spec/test_data/v5.2/expected_potion.rbi +38 -407
  150. data/spec/test_data/v5.2/expected_robe.rbi +38 -409
  151. data/spec/test_data/v5.2/expected_schema_migration.rbi +38 -407
  152. data/spec/test_data/v5.2/expected_school.rbi +47 -407
  153. data/spec/test_data/v5.2/expected_spell.rbi +298 -0
  154. data/spec/test_data/v5.2/expected_spell/habtm_spell_books.rbi +301 -0
  155. data/spec/test_data/v5.2/expected_spell_book.rbi +86 -438
  156. data/spec/test_data/v5.2/expected_spell_book/habtm_spell_books.rbi +649 -0
  157. data/spec/test_data/v5.2/expected_spell_book/habtm_spells.rbi +301 -0
  158. data/spec/test_data/v5.2/expected_squib.rbi +133 -488
  159. data/spec/test_data/v5.2/expected_subject.rbi +298 -0
  160. data/spec/test_data/v5.2/expected_subject/habtm_wizards.rbi +301 -0
  161. data/spec/test_data/v5.2/expected_wand.rbi +75 -448
  162. data/spec/test_data/v5.2/expected_wizard.rbi +155 -509
  163. data/spec/test_data/v5.2/expected_wizard/habtm_subjects.rbi +301 -0
  164. data/spec/test_data/v5.2/expected_wizard_wo_spellbook.rbi +149 -509
  165. data/spec/test_data/v6.0/expected_active_record_base.rbi +113 -0
  166. data/spec/test_data/v6.0/expected_active_record_relation.rbi +175 -0
  167. data/spec/test_data/v6.0/expected_attachment.rbi +38 -431
  168. data/spec/test_data/v6.0/expected_blob.rbi +60 -450
  169. data/spec/test_data/v6.0/expected_habtm_subjects.rbi +720 -0
  170. data/spec/test_data/v6.0/expected_habtm_wizards.rbi +720 -0
  171. data/spec/test_data/v6.0/expected_headmaster.rbi +334 -0
  172. data/spec/test_data/v6.0/expected_internal_metadata.rbi +38 -431
  173. data/spec/test_data/v6.0/expected_potion.rbi +38 -431
  174. data/spec/test_data/v6.0/expected_robe.rbi +38 -433
  175. data/spec/test_data/v6.0/expected_routes.rbi +14 -7
  176. data/spec/test_data/v6.0/expected_schema_migration.rbi +38 -431
  177. data/spec/test_data/v6.0/expected_school.rbi +47 -431
  178. data/spec/test_data/v6.0/expected_spell.rbi +322 -0
  179. data/spec/test_data/v6.0/expected_spell/habtm_spell_books.rbi +325 -0
  180. data/spec/test_data/v6.0/expected_spell_book.rbi +98 -474
  181. data/spec/test_data/v6.0/expected_spell_book/habtm_spell_books.rbi +697 -0
  182. data/spec/test_data/v6.0/expected_spell_book/habtm_spells.rbi +325 -0
  183. data/spec/test_data/v6.0/expected_squib.rbi +162 -541
  184. data/spec/test_data/v6.0/expected_subject.rbi +322 -0
  185. data/spec/test_data/v6.0/expected_subject/habtm_wizards.rbi +325 -0
  186. data/spec/test_data/v6.0/expected_wand.rbi +91 -488
  187. data/spec/test_data/v6.0/expected_wizard.rbi +184 -562
  188. data/spec/test_data/v6.0/expected_wizard/habtm_subjects.rbi +325 -0
  189. data/spec/test_data/v6.0/expected_wizard_wo_spellbook.rbi +178 -562
  190. metadata +170 -24
  191. data/lib/bundled_rbi/active_record_base.rbi +0 -83
  192. data/lib/bundled_rbi/active_record_relation.rbi +0 -122
  193. data/lib/bundled_rbi/parameters.rbi +0 -28
  194. data/lib/sorbet-rails/custom_types/boolean_string.rb +0 -32
  195. data/lib/sorbet-rails/custom_types/integer_string.rb +0 -35
  196. data/lib/sorbet-rails/model_plugins/active_record_finder_methods.rb +0 -131
  197. data/lib/sorbet-rails/rails_mixins/custom_params_methods.rb +0 -46
  198. data/spec/boolean_string_spec.rb +0 -59
  199. data/spec/custom_params_methods_spec.rb +0 -138
  200. data/spec/integer_string_spec.rb +0 -46
  201. data/spec/support/v5.0/typed-override.yaml +0 -2
  202. data/spec/support/v5.1/typed-override.yaml +0 -2
  203. data/spec/support/v5.2/typed-override.yaml +0 -2
  204. data/spec/support/v6.0/typed-override.yaml +0 -2
@@ -12,8 +12,9 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
12
12
  sig { returns(String) }
13
13
  def to_s
14
14
  type = base_type.to_s
15
- type = "T.nilable(#{type})" if nilable
15
+ # A nullable array column should be T.nilable(T::Array[column_type]) not T::Array[T.nilable(column_type)]
16
16
  type = "T::Array[#{type}]" if array_type
17
+ type = "T.nilable(#{type})" if nilable
17
18
  type
18
19
  end
19
20
  end
@@ -25,7 +26,6 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
25
26
 
26
27
  attribute_module_name = self.model_module_name("GeneratedAttributeMethods")
27
28
  attribute_module_rbi = root.create_module(attribute_module_name)
28
- attribute_module_rbi.create_extend("T::Sig")
29
29
 
30
30
  model_class_rbi = root.create_class(self.model_class_name)
31
31
  model_class_rbi.create_include(attribute_module_name)
@@ -82,6 +82,7 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
82
82
  column_def
83
83
  )
84
84
  should_skip_setter_getter = false
85
+ nilable_column = nilable_column?(column_def)
85
86
 
86
87
  if @model_class.is_a?(::ActiveRecord::Enum)
87
88
  config = @model_class.typed_enum_reflections[column_name]
@@ -89,19 +90,19 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
89
90
  # do not generate the methods for enums in strict_mode
90
91
  should_skip_setter_getter = config.strict_mode
91
92
 
92
- t_enum_type = "#{@model_class.name}::#{config.class_name}"
93
+ t_enum_type = "#{model_class_name}::#{config.class_name}"
93
94
 
94
95
  # define T::Enum class & values
95
96
  enum_values = T.must(model_defined_enums[column_name])
96
97
  t_enum_values = @model_class.gen_typed_enum_values(enum_values.keys)
97
98
  root.create_enum_class(
98
99
  t_enum_type,
99
- enums: t_enum_values.map { |k, v| [v, "'#{k}'"] },
100
+ enums: t_enum_values.map { |k, v| [v, "%q{#{k}}"] },
100
101
  )
101
102
 
102
103
  # define t_enum setter/getter
103
104
  assignable_type = t_enum_type
104
- assignable_type = "T.nilable(#{assignable_type})" if column_def.null
105
+ assignable_type = "T.nilable(#{assignable_type})" if nilable_column
105
106
  # add directly to model_class_rbi because they are included
106
107
  # by sorbet's hidden.rbi
107
108
  model_class_rbi.create_method(
@@ -121,9 +122,9 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
121
122
  if !should_skip_setter_getter
122
123
  # enum attribute is treated differently
123
124
  assignable_type = "T.any(Integer, String, Symbol)"
124
- assignable_type = "T.nilable(#{assignable_type})" if column_def.null
125
+ assignable_type = "T.nilable(#{assignable_type})" if nilable_column
125
126
  return_type = "String"
126
- return_type = "T.nilable(#{return_type})" if column_def.null
127
+ return_type = "T.nilable(#{return_type})" if nilable_column
127
128
 
128
129
  attribute_module_rbi.create_method(
129
130
  column_name.to_s,
@@ -145,6 +146,11 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
145
146
  ActiveRecord::Base.connection.lookup_cast_type_from_column(column_def) :
146
147
  column_def.cast_type
147
148
 
149
+ array_type = false
150
+ if column_def.try(:array?)
151
+ cast_type = cast_type.subtype if cast_type.try(:subtype)
152
+ array_type = true
153
+ end
148
154
  strict_type =
149
155
  active_record_type_to_sorbet_type(
150
156
  cast_type,
@@ -153,8 +159,8 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
153
159
 
154
160
  ColumnType.new(
155
161
  base_type: strict_type,
156
- nilable: column_def.null,
157
- array_type: column_def.try(:array?),
162
+ nilable: nilable_column?(column_def),
163
+ array_type: array_type,
158
164
  )
159
165
  end
160
166
 
@@ -214,6 +220,11 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
214
220
  )
215
221
  end
216
222
 
223
+ sig { params(column_def: T.untyped).returns(T::Boolean) }
224
+ def nilable_column?(column_def)
225
+ !!(column_def.null && !attribute_has_unconditional_presence_validation?(column_def.name))
226
+ end
227
+
217
228
  sig { params(column_type: ColumnType).returns(String) }
218
229
  def value_type_for_attr_writer(column_type)
219
230
  # it's safe - and convenient - to assign any "time like" object to a time zone
@@ -9,7 +9,6 @@ class SorbetRails::ModelPlugins::ActiveRecordEnum < SorbetRails::ModelPlugins::B
9
9
 
10
10
  enum_module_name = model_module_name("EnumInstanceMethods")
11
11
  enum_module_rbi = root.create_module(enum_module_name)
12
- enum_module_rbi.create_extend("T::Sig")
13
12
 
14
13
  model_class_rbi = root.create_class(self.model_class_name)
15
14
  model_class_rbi.create_include(enum_module_name)
@@ -7,15 +7,24 @@ class SorbetRails::ModelPlugins::ActiveRecordNamedScope < SorbetRails::ModelPlug
7
7
  def generate(root)
8
8
  model_class_rbi = root.create_class(self.model_class_name)
9
9
 
10
- @model_class.methods.sort.each do |method_name|
10
+ # Named scope methods are dynamically defined by the `scope` method so their
11
+ # source_location is `lib/active_record/scoping/named.rb`. So we find scopes
12
+ # by two criteria:
13
+ # - they are defined in 'activerecord/lib/active_record/scoping/named.rb'
14
+ # - they are not one of the methods actually defined in that file's source.
15
+ # See: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/scoping/named.rb
16
+ non_dynamic_methods = (
17
+ ActiveRecord::Scoping::Named::ClassMethods.instance_methods +
18
+ ActiveRecord::Scoping::Named::ClassMethods.protected_instance_methods +
19
+ ActiveRecord::Scoping::Named::ClassMethods.private_instance_methods
20
+ )
21
+
22
+ (@model_class.methods - non_dynamic_methods).sort.each do |method_name|
11
23
  next unless SorbetRails::Utils.valid_method_name?(method_name.to_s)
24
+
12
25
  method_obj = @model_class.method(method_name)
13
26
  next unless method_obj.present? && method_obj.source_location.present?
14
- # we detect sscopes defined in a model by 2 criteria:
15
- # - they don't have an owner name
16
- # - they are defined in 'activerecord/lib/active_record/scoping/named.rb'
17
- # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/scoping/named.rb
18
- next unless method_obj.owner.name == nil
27
+
19
28
  source_file = method_obj.source_location[0]
20
29
  next unless source_file.include?("lib/active_record/scoping/named.rb")
21
30
 
@@ -1,4 +1,4 @@
1
- # typed: strong
1
+ # typed: strict
2
2
  require ('sorbet-rails/model_plugins/base')
3
3
  class SorbetRails::ModelPlugins::ActiveRecordQuerying < SorbetRails::ModelPlugins::Base
4
4
 
@@ -9,6 +9,7 @@ class SorbetRails::ModelPlugins::ActiveRecordQuerying < SorbetRails::ModelPlugin
9
9
  add_relation_query_method(
10
10
  root,
11
11
  "all",
12
+ builtin_query_method: true,
12
13
  )
13
14
  add_relation_query_method(
14
15
  root,
@@ -16,6 +17,7 @@ class SorbetRails::ModelPlugins::ActiveRecordQuerying < SorbetRails::ModelPlugin
16
17
  parameters: [
17
18
  Parameter.new("&block", type: "T.nilable(T.proc.void)"),
18
19
  ],
20
+ builtin_query_method: true,
19
21
  )
20
22
 
21
23
  # It's not possible to typedef all methods in ActiveRecord::Querying module to have the
@@ -34,6 +36,7 @@ class SorbetRails::ModelPlugins::ActiveRecordQuerying < SorbetRails::ModelPlugin
34
36
  parameters: [
35
37
  Parameter.new("*args", type: "T.untyped"),
36
38
  ],
39
+ builtin_query_method: true,
37
40
  ) if exists_class_method?(method_name)
38
41
  end
39
42
 
@@ -43,7 +46,35 @@ class SorbetRails::ModelPlugins::ActiveRecordQuerying < SorbetRails::ModelPlugin
43
46
  parameters: [
44
47
  Parameter.new("*args", type: "T.untyped"),
45
48
  Parameter.new("&block", type: "T.nilable(T.proc.void)"),
46
- ]
49
+ ],
50
+ builtin_query_method: true,
51
+ )
52
+
53
+ # These are technically "query methods" but they aren't chainable so instead of
54
+ # adding conditionals to `add_relation_query_method` to handle this we'll just
55
+ # handle them here.
56
+ relation_module_rbi = root.create_module(self.model_query_methods_returning_relation_module_name)
57
+ create_in_batches_method(relation_module_rbi, inner_type: self.model_relation_class_name)
58
+
59
+ assoc_relation_module_rbi = root.create_module(self.model_query_methods_returning_assoc_relation_module_name)
60
+ create_in_batches_method(assoc_relation_module_rbi, inner_type: self.model_assoc_relation_class_name)
61
+ end
62
+
63
+ private
64
+
65
+ sig { params(root: Parlour::RbiGenerator::Namespace, inner_type: String).void }
66
+ def create_in_batches_method(root, inner_type:)
67
+ root.create_method(
68
+ "in_batches",
69
+ parameters: [
70
+ Parameter.new("of:", type: "T.nilable(Integer)", default: "1000"),
71
+ Parameter.new("start:", type: "T.nilable(Integer)", default: "nil"),
72
+ Parameter.new("finish:", type: "T.nilable(Integer)", default: "nil"),
73
+ Parameter.new("load:", type: "T.nilable(T::Boolean)", default: "false"),
74
+ Parameter.new("error_on_ignore:", type: "T.nilable(T::Boolean)", default: "nil"),
75
+ Parameter.new("&block", type: "T.nilable(T.proc.params(e: #{inner_type}).void)"),
76
+ ],
77
+ return_type: "ActiveRecord::Batches::BatchEnumerator",
47
78
  )
48
79
  end
49
- end
80
+ end
@@ -52,7 +52,7 @@ class SorbetRails::ModelPlugins::ActiveStorageMethods < SorbetRails::ModelPlugin
52
52
  mod.create_method(
53
53
  "#{assoc_name}=",
54
54
  parameters: [
55
- Parameter.new('*attachables', type: 'T.untyped')
55
+ Parameter.new('attachables', type: 'T.untyped')
56
56
  ],
57
57
  return_type: 'T.untyped'
58
58
  )
@@ -29,5 +29,15 @@ module SorbetRails::ModelPlugins
29
29
  @model_class = T.let(model_class, T.class_of(ActiveRecord::Base))
30
30
  @available_classes = T.let(available_classes, T::Set[String])
31
31
  end
32
+
33
+ sig { params(attribute: T.any(String, Symbol)).returns(T::Boolean) }
34
+ def attribute_has_unconditional_presence_validation?(attribute)
35
+ model_class.validators_on(attribute).any? do |validator|
36
+ validator.is_a?(ActiveModel::Validations::PresenceValidator) &&
37
+ !validator.options.key?(:if) &&
38
+ !validator.options.key?(:unless) &&
39
+ !validator.options.key?(:on)
40
+ end
41
+ end
32
42
  end
33
43
  end
@@ -4,16 +4,7 @@ class SorbetRails::ModelPlugins::EnumerableCollections < SorbetRails::ModelPlugi
4
4
 
5
5
  sig { override.params(root: Parlour::RbiGenerator::Namespace).void }
6
6
  def generate(root)
7
- # model relation, assocation 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, include_methods: false)
11
-
12
- model_assoc_relation_rbi = root.create_class(self.model_assoc_relation_class_name)
13
- create_enumerable_methods_for(model_assoc_relation_rbi)
14
-
15
7
  model_assoc_proxy_class_rbi = root.create_class(self.model_assoc_proxy_class_name)
16
- create_enumerable_methods_for(model_assoc_proxy_class_rbi)
17
8
 
18
9
  # following methods only exists in an association proxy
19
10
  ["<<", "append", "push", "concat"].each do |method_name|
@@ -27,45 +18,4 @@ class SorbetRails::ModelPlugins::EnumerableCollections < SorbetRails::ModelPlugi
27
18
  )
28
19
  end
29
20
  end
30
-
31
- sig { params(class_rbi: Parlour::RbiGenerator::ClassNamespace, include_methods: T::Boolean).void }
32
- def create_enumerable_methods_for(class_rbi, include_methods: true)
33
- class_rbi.create_include("Enumerable")
34
-
35
- # An escape hatch that prevents these methods from being added unnecessarily.
36
- # In certain cases, we can remove these from the classes because they can
37
- # be generalized to use Elem in a superclass instead.
38
- if include_methods
39
- class_rbi.create_method(
40
- "each",
41
- parameters: [
42
- Parameter.new("&block", type: "T.proc.params(e: #{self.model_class_name}).void")
43
- ],
44
- return_type: "T::Array[#{self.model_class_name}]",
45
- implementation: true,
46
- )
47
- class_rbi.create_method(
48
- "flatten",
49
- parameters: [ Parameter.new("level", type: "T.nilable(Integer)") ],
50
- return_type: "T::Array[#{self.model_class_name}]",
51
- )
52
- # this is an escape hatch when there are conflicts in signatures of Enumerable & ActiveRecord
53
- class_rbi.create_method(
54
- "to_a",
55
- return_type: "T::Array[#{self.model_class_name}]",
56
- )
57
- # TODO use type_parameters(:U) when parlour supports it
58
- class_rbi.create_arbitrary(
59
- code: <<~RUBY
60
- sig do
61
- type_parameters(:U).params(
62
- blk: T.proc.params(arg0: Elem).returns(T.type_parameter(:U)),
63
- )
64
- .returns(T::Array[T.type_parameter(:U)])
65
- end
66
- def map(&blk); end
67
- RUBY
68
- )
69
- end
70
- end
71
21
  end
@@ -6,7 +6,6 @@ require('sorbet-rails/model_plugins/active_relation_where_not')
6
6
  require('sorbet-rails/model_plugins/active_record_named_scope')
7
7
  require('sorbet-rails/model_plugins/active_record_attribute')
8
8
  require('sorbet-rails/model_plugins/active_record_assoc')
9
- require('sorbet-rails/model_plugins/active_record_finder_methods')
10
9
  require('sorbet-rails/model_plugins/custom_finder_methods')
11
10
  require('sorbet-rails/model_plugins/enumerable_collections')
12
11
  require('sorbet-rails/model_plugins/active_storage_methods')
@@ -52,8 +51,6 @@ module SorbetRails::ModelPlugins
52
51
  ActiveRecordAttribute
53
52
  when :active_record_assoc
54
53
  ActiveRecordAssoc
55
- when :active_record_finder_methods
56
- ActiveRecordFinderMethods
57
54
  when :custom_finder_methods
58
55
  CustomFinderMethods
59
56
  when :enumerable_collections
@@ -28,13 +28,13 @@ class SorbetRails::ModelRbiFormatter
28
28
  # Load all dynamic instance methods of this model by instantiating a fake model
29
29
  @model_class.new unless @model_class.abstract_class?
30
30
  rescue StandardError => err
31
- puts "#{err.class}: Note: Unable to create new instance of #{model_class.name}"
31
+ puts "#{err.class}: Note: Unable to create new instance of #{model_class_name}"
32
32
  end
33
33
  end
34
34
 
35
35
  sig {returns(String)}
36
36
  def generate_rbi
37
- puts "-- Generate sigs for #{@model_class.name} --"
37
+ puts "-- Generate sigs for #{model_class_name} --"
38
38
 
39
39
  # Collect the instances of each plugin into an array
40
40
  plugin_instances = self.class.get_plugins.map do |plugin_klass|
@@ -74,8 +74,7 @@ class SorbetRails::ModelRbiFormatter
74
74
  self.model_relation_class_name,
75
75
  superclass: "ActiveRecord::Relation",
76
76
  )
77
- model_relation_rbi.create_extend("T::Sig")
78
- model_relation_rbi.create_extend("T::Generic")
77
+ model_relation_rbi.create_include(self.model_query_methods_returning_relation_module_name)
79
78
  model_relation_rbi.create_constant(
80
79
  "Elem",
81
80
  value: "type_member(fixed: #{model_class_name})",
@@ -85,8 +84,7 @@ class SorbetRails::ModelRbiFormatter
85
84
  self.model_assoc_relation_class_name,
86
85
  superclass: "ActiveRecord::AssociationRelation",
87
86
  )
88
- model_assoc_relation_rbi.create_extend("T::Sig")
89
- model_assoc_relation_rbi.create_extend("T::Generic")
87
+ model_assoc_relation_rbi.create_include(self.model_query_methods_returning_assoc_relation_module_name)
90
88
  model_assoc_relation_rbi.create_constant(
91
89
  "Elem",
92
90
  value: "type_member(fixed: #{model_class_name})",
@@ -96,8 +94,7 @@ class SorbetRails::ModelRbiFormatter
96
94
  self.model_assoc_proxy_class_name,
97
95
  superclass: "ActiveRecord::Associations::CollectionProxy",
98
96
  )
99
- collection_proxy_rbi.create_extend("T::Sig")
100
- collection_proxy_rbi.create_extend("T::Generic")
97
+ collection_proxy_rbi.create_include(self.model_query_methods_returning_assoc_relation_module_name)
101
98
  collection_proxy_rbi.create_constant(
102
99
  "Elem",
103
100
  value: "type_member(fixed: #{self.model_class_name})",
@@ -107,8 +104,7 @@ class SorbetRails::ModelRbiFormatter
107
104
  self.model_class_name,
108
105
  superclass: T.must(@model_class.superclass).name,
109
106
  )
110
- model_rbi.create_extend("T::Sig")
111
- model_rbi.create_extend("T::Generic")
107
+ model_rbi.create_extend(self.model_query_methods_returning_relation_module_name)
112
108
  model_rbi.create_type_alias(
113
109
  self.model_relation_type_class_name,
114
110
  type: self.model_relation_type_alias
@@ -5,27 +5,46 @@ module SorbetRails::ModelUtils
5
5
 
6
6
  abstract!
7
7
 
8
- sig { abstract.returns(T.class_of(ActiveRecord::Base)) }
8
+ # if we're a HABTM class then model_class is an anonymous class (see the rails link below) and
9
+ # i'm not sure how to explain that to sorbet other than T.class_of(Class).
10
+ sig { abstract.returns(T.any(T.class_of(ActiveRecord::Base), T.class_of(Class))) }
9
11
  def model_class; end
10
12
 
13
+ sig { returns(T::Boolean) }
14
+ def habtm_class?
15
+ # checking the class name seems to be the cleanest way to figure this out, see:
16
+ # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb#L54
17
+ T.must(model_class.name).start_with?('HABTM_')
18
+ end
19
+
11
20
  sig { returns(String) }
12
21
  def model_class_name
13
- "#{model_class.name}"
22
+ model_class.to_s
14
23
  end
15
24
 
16
25
  sig { returns(String) }
17
26
  def model_relation_class_name
18
- "#{model_class.name}::ActiveRecord_Relation"
27
+ "#{model_class_name}::ActiveRecord_Relation"
19
28
  end
20
29
 
21
30
  sig { returns(String) }
22
31
  def model_assoc_proxy_class_name
23
- "#{model_class.name}::ActiveRecord_Associations_CollectionProxy"
32
+ "#{model_class_name}::ActiveRecord_Associations_CollectionProxy"
24
33
  end
25
34
 
26
35
  sig { returns(String) }
27
36
  def model_assoc_relation_class_name
28
- "#{model_class.name}::ActiveRecord_AssociationRelation"
37
+ "#{model_class_name}::ActiveRecord_AssociationRelation"
38
+ end
39
+
40
+ sig { returns(String) }
41
+ def model_query_methods_returning_relation_module_name
42
+ "#{model_class_name}::QueryMethodsReturningRelation"
43
+ end
44
+
45
+ sig { returns(String) }
46
+ def model_query_methods_returning_assoc_relation_module_name
47
+ "#{model_class_name}::QueryMethodsReturningAssociationRelation"
29
48
  end
30
49
 
31
50
  sig { returns(String) }
@@ -46,7 +65,7 @@ module SorbetRails::ModelUtils
46
65
 
47
66
  sig { params(module_name: String).returns(String) }
48
67
  def model_module_name(module_name)
49
- "#{model_class.name}::#{module_name}"
68
+ "#{model_class_name}::#{module_name}"
50
69
  end
51
70
 
52
71
  sig { params(method_name: T.any(String, Symbol)).returns(T::Boolean) }
@@ -64,9 +83,13 @@ module SorbetRails::ModelUtils
64
83
  root: Parlour::RbiGenerator::Namespace,
65
84
  method_name: String,
66
85
  parameters: T.nilable(T::Array[::Parlour::RbiGenerator::Parameter]),
86
+ # This is meant to indicate the method is a rails-provided query method like
87
+ # where, limit, etc and not something like a named scope. It should likely
88
+ # only be set to `true` when called from the ActiveRecordQuerying plugin.
89
+ builtin_query_method: T::Boolean,
67
90
  ).void
68
91
  }
69
- def add_relation_query_method(root, method_name, parameters: nil)
92
+ def add_relation_query_method(root, method_name, parameters: nil, builtin_query_method: false)
70
93
  # a relation querying method will be available on
71
94
  # - model (as a class method)
72
95
  # - activerecord relation
@@ -75,34 +98,58 @@ module SorbetRails::ModelUtils
75
98
  # in case (1) and (2), it returns a Model::ActiveRecord_Relation
76
99
  # in case (3) and (4), it returns a Model::ActiveRecord_AssociationRelation
77
100
 
78
- # force generating these methods because sorbet's hidden-definitions generate & override them
79
- model_class_rbi = root.create_class(self.model_class_name)
80
- model_class_rbi.create_method(
81
- method_name,
82
- parameters: parameters,
83
- return_type: self.model_relation_class_name,
84
- class_method: true,
85
- )
86
-
87
- model_relation_rbi = root.create_class(self.model_relation_class_name)
88
- model_relation_rbi.create_method(
89
- method_name,
90
- parameters: parameters,
91
- return_type: self.model_relation_class_name,
92
- )
93
-
94
- model_assoc_relation_rbi = root.create_class(self.model_assoc_relation_class_name)
95
- model_assoc_relation_rbi.create_method(
96
- method_name,
97
- parameters: parameters,
98
- return_type: self.model_assoc_relation_class_name,
99
- )
100
-
101
- collection_proxy_rbi = root.create_class(self.model_assoc_proxy_class_name)
102
- collection_proxy_rbi.create_method(
103
- method_name,
104
- parameters: parameters,
105
- return_type: self.model_assoc_relation_class_name,
106
- )
101
+ # 'unscoped' is a special case where it always returns a ActiveRecord_Relation
102
+ assoc_return_value = method_name == 'unscoped' ? self.model_relation_class_name : self.model_assoc_relation_class_name
103
+
104
+ # We can put methods onto modules which are extended/included by the model
105
+ # and relation classes which reduces the RBI footprint for an individual
106
+ # model. However, in Rails 5 query methods that come from scopes or enums
107
+ # get overridden in hidden-definitions so we need to explicitly define them
108
+ # on the model and relation classes.
109
+ if builtin_query_method
110
+ relation_module_rbi = root.create_module(self.model_query_methods_returning_relation_module_name)
111
+ relation_module_rbi.create_method(
112
+ method_name,
113
+ parameters: parameters,
114
+ return_type: self.model_relation_class_name,
115
+ )
116
+
117
+ assoc_relation_module_rbi = root.create_module(self.model_query_methods_returning_assoc_relation_module_name)
118
+ assoc_relation_module_rbi.create_method(
119
+ method_name,
120
+ parameters: parameters,
121
+ return_type: assoc_return_value,
122
+ )
123
+ else
124
+ # force generating these methods because sorbet's hidden-definitions generate & override them
125
+ model_class_rbi = root.create_class(self.model_class_name)
126
+ model_class_rbi.create_method(
127
+ method_name,
128
+ parameters: parameters,
129
+ return_type: self.model_relation_class_name,
130
+ class_method: true,
131
+ )
132
+
133
+ model_relation_rbi = root.create_class(self.model_relation_class_name)
134
+ model_relation_rbi.create_method(
135
+ method_name,
136
+ parameters: parameters,
137
+ return_type: self.model_relation_class_name,
138
+ )
139
+
140
+ model_assoc_relation_rbi = root.create_class(self.model_assoc_relation_class_name)
141
+ model_assoc_relation_rbi.create_method(
142
+ method_name,
143
+ parameters: parameters,
144
+ return_type: assoc_return_value,
145
+ )
146
+
147
+ collection_proxy_rbi = root.create_class(self.model_assoc_proxy_class_name)
148
+ collection_proxy_rbi.create_method(
149
+ method_name,
150
+ parameters: parameters,
151
+ return_type: assoc_return_value,
152
+ )
153
+ end
107
154
  end
108
155
  end