sorbet-rails 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +1 -2
  3. data/.gitignore +2 -1
  4. data/.travis.yml +1 -1
  5. data/README.md +51 -4
  6. data/Rakefile +3 -3
  7. data/lib/bundled_rbi/customizabel_rbi_formatter.rbi +29 -0
  8. data/lib/bundled_rbi/pluck_to_tstruct.rbi +2 -1
  9. data/lib/sorbet-rails.rb +1 -1
  10. data/lib/sorbet-rails/config.rb +11 -0
  11. data/lib/sorbet-rails/deprecation.rb +1 -0
  12. data/lib/sorbet-rails/gem_plugins/kaminari_plugin.rb +8 -0
  13. data/lib/sorbet-rails/job_rbi_formatter.rb +73 -62
  14. data/lib/sorbet-rails/mailer_rbi_formatter.rb +40 -27
  15. data/lib/sorbet-rails/model_column_utils.rb +129 -0
  16. data/lib/sorbet-rails/model_plugins/active_record_assoc.rb +28 -1
  17. data/lib/sorbet-rails/model_plugins/active_record_attribute.rb +0 -102
  18. data/lib/sorbet-rails/model_plugins/base.rb +0 -10
  19. data/lib/sorbet-rails/model_utils.rb +5 -2
  20. data/lib/sorbet-rails/rails_mixins/pluck_to_tstruct.rb +17 -7
  21. data/lib/sorbet-rails/sorbet_utils.rb +152 -150
  22. data/lib/sorbet-rails/tasks/rails_rbi.rake +3 -3
  23. data/sorbet-rails.gemspec +1 -1
  24. data/spec/job_rbi_formatter_spec.rb +1 -1
  25. data/spec/pluck_to_tstruct_spec.rb +74 -1
  26. data/spec/rails_helper.rb +2 -2
  27. data/spec/rake_rails_rbi_jobs_spec.rb +20 -0
  28. data/spec/rake_rails_rbi_mailers_spec.rb +21 -0
  29. data/spec/support/v5.0/Gemfile +1 -1
  30. data/spec/support/v5.0/Gemfile.lock +8 -8
  31. data/spec/support/v5.1/Gemfile.lock +6 -6
  32. data/spec/support/v5.2/Gemfile +1 -1
  33. data/spec/support/v5.2/Gemfile.lock +8 -8
  34. data/spec/support/v6.0/.gitignore +6 -0
  35. data/spec/support/v6.0/Gemfile +3 -3
  36. data/spec/support/v6.0/Gemfile.lock +76 -75
  37. data/spec/support/v6.0/bin/bundle +22 -13
  38. data/spec/support/v6.0/config/environments/test.rb +1 -1
  39. data/spec/support/v6.0/tmp/pids/.keep +0 -0
  40. data/spec/test_data/v5.0/expected_custom_application_job.rbi +21 -0
  41. data/spec/test_data/v5.0/expected_custom_application_mailer.rbi +6 -0
  42. data/spec/test_data/v5.0/expected_custom_award_house_point_hourglasses.rbi +21 -0
  43. data/spec/test_data/v5.0/expected_custom_daily_prophet_mailer.rbi +8 -0
  44. data/spec/test_data/v5.0/expected_custom_hogwarts_acceptance_mailer.rbi +21 -0
  45. data/spec/test_data/v5.0/expected_headmaster.rbi +18 -0
  46. data/spec/test_data/v5.0/expected_potion.rbi +9 -0
  47. data/spec/test_data/v5.0/expected_robe.rbi +9 -0
  48. data/spec/test_data/v5.0/expected_school.rbi +9 -0
  49. data/spec/test_data/v5.0/expected_spell/habtm_spell_books.rbi +18 -0
  50. data/spec/test_data/v5.0/expected_spell_book.rbi +9 -0
  51. data/spec/test_data/v5.0/expected_spell_book/habtm_spells.rbi +18 -0
  52. data/spec/test_data/v5.0/expected_squib.rbi +18 -0
  53. data/spec/test_data/v5.0/expected_subject/habtm_wizards.rbi +18 -0
  54. data/spec/test_data/v5.0/expected_wand.rbi +9 -0
  55. data/spec/test_data/v5.0/expected_wizard.rbi +18 -0
  56. data/spec/test_data/v5.0/expected_wizard/habtm_subjects.rbi +18 -0
  57. data/spec/test_data/v5.0/expected_wizard_wo_spellbook.rbi +18 -0
  58. data/spec/test_data/v5.1/expected_custom_application_job.rbi +21 -0
  59. data/spec/test_data/v5.1/expected_custom_application_mailer.rbi +6 -0
  60. data/spec/test_data/v5.1/expected_custom_award_house_point_hourglasses.rbi +21 -0
  61. data/spec/test_data/v5.1/expected_custom_daily_prophet_mailer.rbi +8 -0
  62. data/spec/test_data/v5.1/expected_custom_hogwarts_acceptance_mailer.rbi +21 -0
  63. data/spec/test_data/v5.1/expected_headmaster.rbi +18 -0
  64. data/spec/test_data/v5.1/expected_potion.rbi +9 -0
  65. data/spec/test_data/v5.1/expected_robe.rbi +9 -0
  66. data/spec/test_data/v5.1/expected_school.rbi +9 -0
  67. data/spec/test_data/v5.1/expected_spell/habtm_spell_books.rbi +18 -0
  68. data/spec/test_data/v5.1/expected_spell_book.rbi +9 -0
  69. data/spec/test_data/v5.1/expected_spell_book/habtm_spells.rbi +18 -0
  70. data/spec/test_data/v5.1/expected_squib.rbi +18 -0
  71. data/spec/test_data/v5.1/expected_subject/habtm_wizards.rbi +18 -0
  72. data/spec/test_data/v5.1/expected_wand.rbi +9 -0
  73. data/spec/test_data/v5.1/expected_wizard.rbi +18 -0
  74. data/spec/test_data/v5.1/expected_wizard/habtm_subjects.rbi +18 -0
  75. data/spec/test_data/v5.1/expected_wizard_wo_spellbook.rbi +18 -0
  76. data/spec/test_data/v5.2/expected_attachment.rbi +18 -0
  77. data/spec/test_data/v5.2/expected_blob.rbi +19 -1
  78. data/spec/test_data/v5.2/expected_custom_application_job.rbi +21 -0
  79. data/spec/test_data/v5.2/expected_custom_application_mailer.rbi +6 -0
  80. data/spec/test_data/v5.2/expected_custom_award_house_point_hourglasses.rbi +21 -0
  81. data/spec/test_data/v5.2/expected_custom_daily_prophet_mailer.rbi +8 -0
  82. data/spec/test_data/v5.2/expected_custom_hogwarts_acceptance_mailer.rbi +21 -0
  83. data/spec/test_data/v5.2/expected_headmaster.rbi +18 -0
  84. data/spec/test_data/v5.2/expected_potion.rbi +9 -0
  85. data/spec/test_data/v5.2/expected_robe.rbi +9 -0
  86. data/spec/test_data/v5.2/expected_school.rbi +9 -0
  87. data/spec/test_data/v5.2/expected_spell/habtm_spell_books.rbi +18 -0
  88. data/spec/test_data/v5.2/expected_spell_book.rbi +9 -0
  89. data/spec/test_data/v5.2/expected_spell_book/habtm_spells.rbi +18 -0
  90. data/spec/test_data/v5.2/expected_squib.rbi +38 -2
  91. data/spec/test_data/v5.2/expected_subject/habtm_wizards.rbi +18 -0
  92. data/spec/test_data/v5.2/expected_wand.rbi +9 -0
  93. data/spec/test_data/v5.2/expected_wizard.rbi +38 -2
  94. data/spec/test_data/v5.2/expected_wizard/habtm_subjects.rbi +18 -0
  95. data/spec/test_data/v5.2/expected_wizard_wo_spellbook.rbi +38 -2
  96. data/spec/test_data/v6.0/expected_attachment.rbi +18 -0
  97. data/spec/test_data/v6.0/expected_blob.rbi +19 -1
  98. data/spec/test_data/v6.0/expected_custom_application_job.rbi +21 -0
  99. data/spec/test_data/v6.0/expected_custom_application_mailer.rbi +6 -0
  100. data/spec/test_data/v6.0/expected_custom_award_house_point_hourglasses.rbi +21 -0
  101. data/spec/test_data/v6.0/expected_custom_daily_prophet_mailer.rbi +8 -0
  102. data/spec/test_data/v6.0/expected_custom_hogwarts_acceptance_mailer.rbi +21 -0
  103. data/spec/test_data/v6.0/expected_headmaster.rbi +18 -0
  104. data/spec/test_data/v6.0/expected_potion.rbi +9 -0
  105. data/spec/test_data/v6.0/expected_robe.rbi +9 -0
  106. data/spec/test_data/v6.0/expected_school.rbi +9 -0
  107. data/spec/test_data/v6.0/expected_spell/habtm_spell_books.rbi +18 -0
  108. data/spec/test_data/v6.0/expected_spell_book.rbi +9 -0
  109. data/spec/test_data/v6.0/expected_spell_book/habtm_spells.rbi +18 -0
  110. data/spec/test_data/v6.0/expected_squib.rbi +38 -2
  111. data/spec/test_data/v6.0/expected_subject/habtm_wizards.rbi +18 -0
  112. data/spec/test_data/v6.0/expected_wand.rbi +9 -0
  113. data/spec/test_data/v6.0/expected_wizard.rbi +38 -2
  114. data/spec/test_data/v6.0/expected_wizard/habtm_subjects.rbi +18 -0
  115. data/spec/test_data/v6.0/expected_wizard_wo_spellbook.rbi +38 -2
  116. metadata +45 -1
@@ -0,0 +1,129 @@
1
+ # typed: strict
2
+ module SorbetRails::ModelColumnUtils
3
+ extend T::Sig
4
+ extend T::Helpers
5
+
6
+ abstract!
7
+
8
+ class ColumnType < T::Struct
9
+ extend T::Sig
10
+
11
+ const :base_type, T.any(Class, String)
12
+ const :nilable, T.nilable(T::Boolean)
13
+ const :array_type, T.nilable(T::Boolean)
14
+
15
+ sig { returns(String) }
16
+ def to_s
17
+ type = base_type.to_s
18
+ # A nullable array column should be T.nilable(T::Array[column_type]) not T::Array[T.nilable(column_type)]
19
+ type = "T::Array[#{type}]" if array_type
20
+ type = "T.nilable(#{type})" if nilable
21
+ type
22
+ end
23
+ end
24
+
25
+ # if we're a HABTM class then model_class is an anonymous class (see the rails link below) and
26
+ # i'm not sure how to explain that to sorbet other than T.class_of(Class).
27
+ sig { abstract.returns(T.any(T.class_of(ActiveRecord::Base), T.class_of(Class))) }
28
+ def model_class; end
29
+
30
+ sig { params(column_def: T.untyped).returns(ColumnType) }
31
+ def type_for_column_def(column_def)
32
+ cast_type = ActiveRecord::Base.connection.respond_to?(:lookup_cast_type_from_column) ?
33
+ ActiveRecord::Base.connection.lookup_cast_type_from_column(column_def) :
34
+ column_def.cast_type
35
+
36
+ array_type = false
37
+ if column_def.try(:array?)
38
+ cast_type = cast_type.subtype if cast_type.try(:subtype)
39
+ array_type = true
40
+ end
41
+ strict_type =
42
+ active_record_type_to_sorbet_type(
43
+ cast_type,
44
+ time_zone_aware: time_zone_aware_column?(column_def, cast_type),
45
+ )
46
+
47
+ ColumnType.new(
48
+ base_type: strict_type,
49
+ nilable: nilable_column?(column_def),
50
+ array_type: array_type,
51
+ )
52
+ end
53
+
54
+ # True if this column is "time zone aware", which means it'll be converted on
55
+ # access from its original class (e.g. `DateTime`) to something with better
56
+ # support for time zones (usually `ActiveSupport::TimeWithZone`)
57
+ sig do
58
+ params(column_def: T.untyped, cast_type: T.untyped).returns(T::Boolean)
59
+ end
60
+ def time_zone_aware_column?(column_def, cast_type)
61
+ # this private class method returns true if the attribute should be "time
62
+ # zone aware"; it takes into account various global and model-specific
63
+ # configuration options as described here:
64
+ # https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html
65
+ #
66
+ # although it's private, it's better this than trying to reimplement the "is
67
+ # this attribute tz aware?" logic ourselves
68
+ model_class.send(
69
+ :create_time_zone_conversion_attribute?,
70
+ column_def.name,
71
+ cast_type
72
+ )
73
+ end
74
+
75
+ sig { params(column_def: T.untyped).returns(T::Boolean) }
76
+ def nilable_column?(column_def)
77
+ !!(column_def.null && !attribute_has_unconditional_presence_validation?(column_def.name))
78
+ end
79
+
80
+ sig do
81
+ params(
82
+ klass: Object,
83
+ time_zone_aware: T::Boolean,
84
+ ).returns(T.any(String, Class))
85
+ end
86
+ def active_record_type_to_sorbet_type(klass, time_zone_aware: false)
87
+ case klass
88
+ when ActiveRecord::Type::Boolean
89
+ "T::Boolean"
90
+ when ActiveRecord::Type::DateTime, ActiveRecord::Type::Time
91
+ time_zone_aware ? ActiveSupport::TimeWithZone : Time
92
+ when ActiveRecord::Type::Date
93
+ Date
94
+ when ActiveRecord::Type::Decimal
95
+ BigDecimal
96
+ when ActiveRecord::Type::Float
97
+ Float
98
+ when ActiveRecord::Type::BigInteger, ActiveRecord::Type::Integer, ActiveRecord::Type::DecimalWithoutScale, ActiveRecord::Type::UnsignedInteger
99
+ Integer
100
+ when ActiveRecord::Type::Binary, ActiveRecord::Type::String, ActiveRecord::Type::Text
101
+ String
102
+ else
103
+ # Json type is only supported in Rails 5.2 and above
104
+ case
105
+ when Object.const_defined?('ActiveRecord::Type::Json') && klass.is_a?(ActiveRecord::Type::Json)
106
+ "T.any(T::Array[T.untyped], T::Boolean, Float, T::Hash[T.untyped, T.untyped], Integer, String)"
107
+ when Object.const_defined?('ActiveRecord::Enum::EnumType') && klass.is_a?(ActiveRecord::Enum::EnumType)
108
+ String
109
+ # For Postgres UUIDs, they're represented as Strings
110
+ when Object.const_defined?('ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid') && klass.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid)
111
+ String
112
+ else
113
+ "T.untyped"
114
+ end
115
+ end
116
+ end
117
+
118
+ sig { params(attribute: T.any(String, Symbol)).returns(T::Boolean) }
119
+ def attribute_has_unconditional_presence_validation?(attribute)
120
+ model_class_srb = model_class
121
+ model_class_srb < ActiveRecord::Base &&
122
+ model_class_srb.validators_on(attribute).any? do |validator|
123
+ validator.is_a?(ActiveModel::Validations::PresenceValidator) &&
124
+ !validator.options.key?(:if) &&
125
+ !validator.options.key?(:unless) &&
126
+ !validator.options.key?(:on)
127
+ end
128
+ end
129
+ end
@@ -36,10 +36,30 @@ class SorbetRails::ModelPlugins::ActiveRecordAssoc < SorbetRails::ModelPlugins::
36
36
  assoc_class = assoc_should_be_untyped?(reflection) ? "T.untyped" : "::#{reflection.klass.name}"
37
37
  assoc_type = (belongs_to_and_required?(reflection) || has_one_and_required?(reflection)) ? assoc_class : "T.nilable(#{assoc_class})"
38
38
 
39
+ params = [
40
+ Parameter.new("*args", type: "T.untyped"),
41
+ Parameter.new("&block", type: "T.nilable(T.proc.params(object: #{assoc_class}).void)")
42
+ ]
43
+
39
44
  assoc_module_rbi.create_method(
40
45
  assoc_name.to_s,
41
46
  return_type: assoc_type,
42
47
  )
48
+ assoc_module_rbi.create_method(
49
+ "build_#{assoc_name}",
50
+ parameters: params,
51
+ return_type: assoc_class,
52
+ )
53
+ assoc_module_rbi.create_method(
54
+ "create_#{assoc_name}",
55
+ parameters: params,
56
+ return_type: assoc_class,
57
+ )
58
+ assoc_module_rbi.create_method(
59
+ "create_#{assoc_name}!",
60
+ parameters: params,
61
+ return_type: assoc_class,
62
+ )
43
63
  assoc_module_rbi.create_method(
44
64
  "#{assoc_name}=",
45
65
  parameters: [
@@ -114,9 +134,16 @@ class SorbetRails::ModelPlugins::ActiveRecordAssoc < SorbetRails::ModelPlugins::
114
134
  return_type: relation_class,
115
135
  )
116
136
  unless assoc_should_be_untyped?(reflection)
137
+ if reflection.klass.table_exists?
138
+ # Normally the id_type is an Integer, but it could be a String if using
139
+ # UUIDs.
140
+ id_type = type_for_column_def(reflection.klass.columns_hash["id"]).to_s
141
+ else
142
+ id_type = "T.untyped"
143
+ end
117
144
  assoc_module_rbi.create_method(
118
145
  "#{assoc_name.singularize}_ids",
119
- return_type: "T::Array[Integer]",
146
+ return_type: "T::Array[#{id_type}]",
120
147
  )
121
148
  end
122
149
  assoc_module_rbi.create_method(
@@ -2,23 +2,6 @@
2
2
  require ('sorbet-rails/model_plugins/base')
3
3
  class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugins::Base
4
4
 
5
- class ColumnType < T::Struct
6
- extend T::Sig
7
-
8
- const :base_type, T.any(Class, String)
9
- const :nilable, T.nilable(T::Boolean)
10
- const :array_type, T.nilable(T::Boolean)
11
-
12
- sig { returns(String) }
13
- def to_s
14
- type = base_type.to_s
15
- # A nullable array column should be T.nilable(T::Array[column_type]) not T::Array[T.nilable(column_type)]
16
- type = "T::Array[#{type}]" if array_type
17
- type = "T.nilable(#{type})" if nilable
18
- type
19
- end
20
- end
21
-
22
5
  sig { override.params(root: Parlour::RbiGenerator::Namespace).void }
23
6
  def generate(root)
24
7
  columns_hash = @model_class.table_exists? ? @model_class.columns_hash : {}
@@ -140,91 +123,6 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
140
123
  end
141
124
  end
142
125
 
143
- sig { params(column_def: T.untyped).returns(ColumnType) }
144
- def type_for_column_def(column_def)
145
- cast_type = ActiveRecord::Base.connection.respond_to?(:lookup_cast_type_from_column) ?
146
- ActiveRecord::Base.connection.lookup_cast_type_from_column(column_def) :
147
- column_def.cast_type
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
154
- strict_type =
155
- active_record_type_to_sorbet_type(
156
- cast_type,
157
- time_zone_aware: time_zone_aware_column?(column_def, cast_type),
158
- )
159
-
160
- ColumnType.new(
161
- base_type: strict_type,
162
- nilable: nilable_column?(column_def),
163
- array_type: array_type,
164
- )
165
- end
166
-
167
- sig do
168
- params(
169
- klass: Object,
170
- time_zone_aware: T::Boolean,
171
- ).returns(T.any(String, Class))
172
- end
173
- def active_record_type_to_sorbet_type(klass, time_zone_aware: false)
174
- case klass
175
- when ActiveRecord::Type::Boolean
176
- "T::Boolean"
177
- when ActiveRecord::Type::DateTime, ActiveRecord::Type::Time
178
- time_zone_aware ? ActiveSupport::TimeWithZone : Time
179
- when ActiveRecord::Type::Date
180
- Date
181
- when ActiveRecord::Type::Decimal
182
- BigDecimal
183
- when ActiveRecord::Type::Float
184
- Float
185
- when ActiveRecord::Type::BigInteger, ActiveRecord::Type::Integer, ActiveRecord::Type::DecimalWithoutScale, ActiveRecord::Type::UnsignedInteger
186
- Integer
187
- when ActiveRecord::Type::Binary, ActiveRecord::Type::String, ActiveRecord::Type::Text
188
- String
189
- else
190
- # Json type is only supported in Rails 5.2 and above
191
- case
192
- when Object.const_defined?('ActiveRecord::Type::Json') && klass.is_a?(ActiveRecord::Type::Json)
193
- "T.any(T::Array[T.untyped], T::Boolean, Float, T::Hash[T.untyped, T.untyped], Integer, String)"
194
- when Object.const_defined?('ActiveRecord::Enum::EnumType') && klass.is_a?(ActiveRecord::Enum::EnumType)
195
- String
196
- else
197
- "T.untyped"
198
- end
199
- end
200
- end
201
-
202
- # True if this column is "time zone aware", which means it'll be converted on
203
- # access from its original class (e.g. `DateTime`) to something with better
204
- # support for time zones (usually `ActiveSupport::TimeWithZone`)
205
- sig do
206
- params(column_def: T.untyped, cast_type: T.untyped).returns(T::Boolean)
207
- end
208
- def time_zone_aware_column?(column_def, cast_type)
209
- # this private class method returns true if the attribute should be "time
210
- # zone aware"; it takes into account various global and model-specific
211
- # configuration options as described here:
212
- # https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html
213
- #
214
- # although it's private, it's better this than trying to reimplement the "is
215
- # this attribute tz aware?" logic ourselves
216
- @model_class.send(
217
- :create_time_zone_conversion_attribute?,
218
- column_def.name,
219
- cast_type
220
- )
221
- end
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
-
228
126
  sig { params(column_type: ColumnType).returns(String) }
229
127
  def value_type_for_attr_writer(column_type)
230
128
  # it's safe - and convenient - to assign any "time like" object to a time zone
@@ -29,15 +29,5 @@ 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
42
32
  end
43
33
  end
@@ -1,14 +1,17 @@
1
1
  # typed: strict
2
+ require('sorbet-rails/model_column_utils')
2
3
  module SorbetRails::ModelUtils
3
4
  extend T::Sig
4
5
  extend T::Helpers
6
+ include SorbetRails::ModelColumnUtils
5
7
 
6
8
  abstract!
7
9
 
8
10
  # if we're a HABTM class then model_class is an anonymous class (see the rails link below) and
9
11
  # 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))) }
11
- def model_class; end
12
+ # This is also defined in ModelColumnUtils
13
+ # sig { abstract.returns(T.any(T.class_of(ActiveRecord::Base), T.class_of(Class))) }
14
+ # def model_class; end
12
15
 
13
16
  sig { returns(T::Boolean) }
14
17
  def habtm_class?
@@ -7,27 +7,37 @@ module SorbetRails::PluckToTStruct
7
7
  type_parameters(:U).
8
8
  params(
9
9
  ta_struct: ITypeAssert[T.type_parameter(:U)],
10
+ associations: T::Hash[Symbol, String],
10
11
  ).
11
12
  returns(T::Array[T.type_parameter(:U)])
12
13
  }
13
- def pluck_to_tstruct(ta_struct)
14
+ def pluck_to_tstruct(ta_struct, associations: {})
14
15
  tstruct = ta_struct.get_type
15
-
16
+
16
17
  if !(tstruct < T::Struct)
17
18
  raise UnexpectedType.new("pluck_to_tstruct expects a tstruct subclass, given #{tstruct}")
18
19
  end
19
20
 
20
- keys = tstruct.props.keys
21
+ tstruct_keys = tstruct.props.keys
22
+ associations_keys = associations.keys
23
+ invalid_keys = associations_keys - tstruct_keys
24
+
25
+ if invalid_keys.any?
26
+ raise UnexpectedAssociations.new("Argument 'associations' contains keys that don't exist in #{tstruct}: #{invalid_keys.join(", ")}")
27
+ end
28
+
29
+ pluck_keys = (tstruct_keys - associations_keys) + associations.values
21
30
 
22
31
  # loosely based on pluck_to_hash gem
23
32
  # https://github.com/girishso/pluck_to_hash/blob/master/lib/pluck_to_hash.rb
24
- keys_one = keys.size == 1
25
- pluck(*keys).map do |row|
33
+ keys_one = pluck_keys.size == 1
34
+ pluck(*pluck_keys).map do |row|
26
35
  row = [row] if keys_one
27
- value = Hash[keys.zip(row)]
36
+ value = Hash[tstruct_keys.zip(row)]
28
37
  tstruct.new(value)
29
38
  end
30
39
  end
31
-
40
+
32
41
  class UnexpectedType < StandardError; end
42
+ class UnexpectedAssociations < StandardError; end
33
43
  end
@@ -5,168 +5,170 @@ require('sorbet-runtime')
5
5
  require('method_source')
6
6
  require('parser/current')
7
7
 
8
- module SorbetRails::SorbetUtils
9
- extend T::Sig
10
- include Kernel
11
-
12
- class ParsedParamDef < T::Struct
13
- const :name, Symbol
14
- const :kind, Symbol
15
- const :type_str, String
16
- prop :default, T.nilable(String), default: nil
17
- prop :prefix, T.nilable(String)
18
- prop :suffix, T.nilable(String)
19
- end
20
-
21
- sig { params(method_def: UnboundMethod).returns(T::Array[Parlour::RbiGenerator::Parameter]) }
22
- def self.parameters_from_method_def(method_def)
23
- signature = T::Private::Methods.signature_for_method(method_def)
24
- method_def = signature.nil? ? method_def : signature.method
25
-
26
- parameters_with_type = signature.nil? ?
27
- method_def.parameters.map { |p|
28
- ParsedParamDef.new(
29
- name: p.size == 1 ? :_ : p[1], # give param without name default name _
30
- kind: p[0], # append untyped as type of each param
31
- type_str: 'T.untyped',
32
- )
33
- } :
34
- get_ordered_parameters_with_type(signature)
35
-
36
- # add prefix & suffix
37
- parameters_with_type.each do |param_def|
38
- param_def.prefix =
39
- case param_def.kind
40
- when :rest; '*'
41
- when :keyrest; '**'
42
- when :block; '&'
43
- # being comprehensive
44
- when :req, :opt; ''
45
- when :key, :keyreq; ''
46
- else nil
47
- end
48
-
49
- param_def.suffix =
50
- case param_def.kind
51
- when :key, :keyreq; ':'
52
- else nil
53
- end
8
+ module SorbetRails
9
+ module SorbetUtils
10
+ extend T::Sig
11
+ include Kernel
12
+
13
+ class ParsedParamDef < T::Struct
14
+ const :name, Symbol
15
+ const :kind, Symbol
16
+ const :type_str, String
17
+ prop :default, T.nilable(String), default: nil
18
+ prop :prefix, T.nilable(String)
19
+ prop :suffix, T.nilable(String)
54
20
  end
55
21
 
56
- extract_default_value_for_params!(
57
- parameters_with_type,
58
- method_def,
59
- )
60
-
61
- parameters_with_type.map do |param_def|
62
- ::Parlour::RbiGenerator::Parameter.new(
63
- "#{param_def.prefix}#{param_def.name}#{param_def.suffix}",
64
- type: param_def.type_str,
65
- default: param_def.default,
22
+ sig { params(method_def: UnboundMethod).returns(T::Array[Parlour::RbiGenerator::Parameter]) }
23
+ def self.parameters_from_method_def(method_def)
24
+ signature = T::Private::Methods.signature_for_method(method_def)
25
+ method_def = signature.nil? ? method_def : signature.method
26
+
27
+ parameters_with_type = signature.nil? ?
28
+ method_def.parameters.map { |p|
29
+ ParsedParamDef.new(
30
+ name: p.size == 1 ? :_ : p[1], # give param without name default name _
31
+ kind: p[0], # append untyped as type of each param
32
+ type_str: 'T.untyped',
33
+ )
34
+ } :
35
+ get_ordered_parameters_with_type(signature)
36
+
37
+ # add prefix & suffix
38
+ parameters_with_type.each do |param_def|
39
+ param_def.prefix =
40
+ case param_def.kind
41
+ when :rest; '*'
42
+ when :keyrest; '**'
43
+ when :block; '&'
44
+ # being comprehensive
45
+ when :req, :opt; ''
46
+ when :key, :keyreq; ''
47
+ else nil
48
+ end
49
+
50
+ param_def.suffix =
51
+ case param_def.kind
52
+ when :key, :keyreq; ':'
53
+ else nil
54
+ end
55
+ end
56
+
57
+ extract_default_value_for_params!(
58
+ parameters_with_type,
59
+ method_def,
66
60
  )
67
- end
68
- end
69
61
 
70
- sig {
71
- params(signature: T::Private::Methods::Signature).
72
- returns(T::Array[ParsedParamDef])
73
- }
74
- def self.get_ordered_parameters_with_type(signature)
75
- # extract original method param from signature
76
- # https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/private/methods/signature.rb#L5-L8
77
- params = T.let([], T::Array[ParsedParamDef])
78
- signature.arg_types.each do |arg_type|
79
- # could be :opt, but doesn't matter
80
- params << ParsedParamDef.new(
81
- name: arg_type[0],
82
- kind: :req,
83
- type_str: arg_type[1].to_s,
84
- )
85
- end
86
- signature.kwarg_types.each do |kwarg_name, kwarg_type|
87
- # could be :key, but doesn't matter
88
- params << ParsedParamDef.new(
89
- name: kwarg_name,
90
- kind: :keyreq,
91
- type_str: kwarg_type.to_s,
92
- )
93
- end
94
- if signature.has_rest
95
- params << ParsedParamDef.new(
96
- name: signature.rest_name,
97
- kind: :rest,
98
- type_str: signature.rest_type.to_s,
99
- )
100
- end
101
- if signature.has_keyrest
102
- params << ParsedParamDef.new(
103
- name: signature.keyrest_name,
104
- kind: :keyrest,
105
- type_str: signature.keyrest_type.to_s,
106
- )
107
- end
108
- if !signature.block_name.nil?
109
- # special case `.void` in a proc
110
- # see https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/types/proc.rb#L10
111
- block_param_type = signature.block_type.to_s
112
- block_param_type = block_param_type.gsub('returns(<VOID>)', 'void')
113
- params << ParsedParamDef.new(
114
- name: signature.block_name,
115
- kind: :block,
116
- type_str: block_param_type,
117
- )
62
+ parameters_with_type.map do |param_def|
63
+ ::Parlour::RbiGenerator::Parameter.new(
64
+ "#{param_def.prefix}#{param_def.name}#{param_def.suffix}",
65
+ type: param_def.type_str,
66
+ default: param_def.default,
67
+ )
68
+ end
118
69
  end
119
- params
120
- end
121
70
 
122
- sig {
123
- params(
124
- parsed_params: T::Array[ParsedParamDef],
125
- method_def: UnboundMethod,
126
- ).void
127
- }
128
- def self.extract_default_value_for_params!(parsed_params, method_def)
129
- source = method_def.source
130
- parsed_ast = Parser::CurrentRuby.parse(source)
131
- if parsed_ast.type != :def
132
- # could be a method added at runtime? ignore it
133
- puts "Warning: unable to parse the source of #{method_def.name}"
134
- return
71
+ sig {
72
+ params(signature: T::Private::Methods::Signature).
73
+ returns(T::Array[ParsedParamDef])
74
+ }
75
+ def self.get_ordered_parameters_with_type(signature)
76
+ # extract original method param from signature
77
+ # https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/private/methods/signature.rb#L5-L8
78
+ params = T.let([], T::Array[ParsedParamDef])
79
+ signature.arg_types.each do |arg_type|
80
+ # could be :opt, but doesn't matter
81
+ params << ParsedParamDef.new(
82
+ name: arg_type[0],
83
+ kind: :req,
84
+ type_str: arg_type[1].to_s,
85
+ )
86
+ end
87
+ signature.kwarg_types.each do |kwarg_name, kwarg_type|
88
+ # could be :key, but doesn't matter
89
+ params << ParsedParamDef.new(
90
+ name: kwarg_name,
91
+ kind: :keyreq,
92
+ type_str: kwarg_type.to_s,
93
+ )
94
+ end
95
+ if signature.has_rest
96
+ params << ParsedParamDef.new(
97
+ name: signature.rest_name,
98
+ kind: :rest,
99
+ type_str: signature.rest_type.to_s,
100
+ )
101
+ end
102
+ if signature.has_keyrest
103
+ params << ParsedParamDef.new(
104
+ name: signature.keyrest_name,
105
+ kind: :keyrest,
106
+ type_str: signature.keyrest_type.to_s,
107
+ )
108
+ end
109
+ if !signature.block_name.nil?
110
+ # special case `.void` in a proc
111
+ # see https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/types/proc.rb#L10
112
+ block_param_type = signature.block_type.to_s
113
+ block_param_type = block_param_type.gsub('returns(<VOID>)', 'void')
114
+ params << ParsedParamDef.new(
115
+ name: signature.block_name,
116
+ kind: :block,
117
+ type_str: block_param_type,
118
+ )
119
+ end
120
+ params
135
121
  end
136
122
 
137
- args = parsed_ast.children[1]
138
- if args.type != :args
139
- puts "Warning: unable to parse the source of #{method_def.name}"
140
- return
123
+ sig {
124
+ params(
125
+ parsed_params: T::Array[ParsedParamDef],
126
+ method_def: UnboundMethod,
127
+ ).void
128
+ }
129
+ def self.extract_default_value_for_params!(parsed_params, method_def)
130
+ source = method_def.source
131
+ parsed_ast = Parser::CurrentRuby.parse(source)
132
+ if parsed_ast.type != :def
133
+ # could be a method added at runtime? ignore it
134
+ puts "Warning: unable to parse the source of #{method_def.name}"
135
+ return
136
+ end
137
+
138
+ args = parsed_ast.children[1]
139
+ if args.type != :args
140
+ puts "Warning: unable to parse the source of #{method_def.name}"
141
+ return
142
+ end
143
+
144
+ parsed_params_map = Hash[parsed_params.map {|p| [p.name, p]}]
145
+ args.children.each do |arg|
146
+ arg_name = arg.children[0]
147
+ default = arg.children[1] ? node_to_s(arg.children[1]) : nil
148
+
149
+ next if arg_name.blank?
150
+
151
+ param_def = parsed_params_map[arg_name]
152
+
153
+ raise UnexpectedParam.new(
154
+ "Unexpected param #{arg_name} when parsing #{method_def.name}"
155
+ ) unless param_def.present?
156
+
157
+ param_def.default = default
158
+ end
141
159
  end
142
160
 
143
- parsed_params_map = Hash[parsed_params.map {|p| [p.name, p]}]
144
- args.children.each do |arg|
145
- arg_name = arg.children[0]
146
- default = arg.children[1] ? node_to_s(arg.children[1]) : nil
147
-
148
- next if arg_name.blank?
149
-
150
- param_def = parsed_params_map[arg_name]
151
-
152
- raise UnexpectedParam.new(
153
- "Unexpected param #{arg_name} when parsing #{method_def.name}"
154
- ) unless param_def.present?
161
+ # Given an AST node, returns the source code from which it was constructed.
162
+ # If the given AST node is nil, this returns nil.
163
+ # Taken from https://github.com/AaronC81/parlour/blob/master/lib/parlour/type_parser.rb#L506
164
+ sig { params(node: T.nilable(Parser::AST::Node)).returns(T.nilable(String)) }
165
+ def self.node_to_s(node)
166
+ return nil unless node
155
167
 
156
- param_def.default = default
168
+ exp = node.loc.expression
169
+ exp.source_buffer.source[exp.begin_pos...exp.end_pos]
157
170
  end
158
- end
159
-
160
- # Given an AST node, returns the source code from which it was constructed.
161
- # If the given AST node is nil, this returns nil.
162
- # Taken from https://github.com/AaronC81/parlour/blob/master/lib/parlour/type_parser.rb#L506
163
- sig { params(node: T.nilable(Parser::AST::Node)).returns(T.nilable(String)) }
164
- def self.node_to_s(node)
165
- return nil unless node
166
171
 
167
- exp = node.loc.expression
168
- exp.source_buffer.source[exp.begin_pos...exp.end_pos]
172
+ class UnexpectedParam < StandardError; end
169
173
  end
170
-
171
- class UnexpectedParam < StandardError; end
172
174
  end