tapioca 0.2.7 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +27 -1
  3. data/README.md +21 -2
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +15 -9
  6. data/lib/tapioca/cli.rb +41 -12
  7. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +129 -0
  8. data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
  9. data/lib/tapioca/compilers/dsl/active_record_associations.rb +285 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +379 -0
  11. data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
  12. data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +213 -0
  13. data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
  14. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +170 -0
  15. data/lib/tapioca/compilers/dsl/active_resource.rb +140 -0
  16. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +126 -0
  17. data/lib/tapioca/compilers/dsl/base.rb +163 -0
  18. data/lib/tapioca/compilers/dsl/frozen_record.rb +96 -0
  19. data/lib/tapioca/compilers/dsl/protobuf.rb +144 -0
  20. data/lib/tapioca/compilers/dsl/smart_properties.rb +173 -0
  21. data/lib/tapioca/compilers/dsl/state_machines.rb +378 -0
  22. data/lib/tapioca/compilers/dsl/url_helpers.rb +83 -0
  23. data/lib/tapioca/compilers/dsl_compiler.rb +121 -0
  24. data/lib/tapioca/compilers/requires_compiler.rb +67 -0
  25. data/lib/tapioca/compilers/sorbet.rb +34 -0
  26. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +209 -49
  27. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +3 -17
  28. data/lib/tapioca/compilers/todos_compiler.rb +32 -0
  29. data/lib/tapioca/config.rb +42 -0
  30. data/lib/tapioca/config_builder.rb +75 -0
  31. data/lib/tapioca/constant_locator.rb +1 -0
  32. data/lib/tapioca/core_ext/class.rb +23 -0
  33. data/lib/tapioca/gemfile.rb +14 -1
  34. data/lib/tapioca/generator.rb +235 -67
  35. data/lib/tapioca/loader.rb +20 -9
  36. data/lib/tapioca/sorbet_config_parser.rb +77 -0
  37. data/lib/tapioca/version.rb +1 -1
  38. metadata +35 -66
@@ -0,0 +1,112 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "active_record"
8
+ rescue LoadError
9
+ # means ActiveRecord is not installed,
10
+ # so let's not even define the generator.
11
+ return
12
+ end
13
+
14
+ module Tapioca
15
+ module Compilers
16
+ module Dsl
17
+ # `Tapioca::Compilers::Dsl::ActiveRecordEnum` decorates RBI files for subclasses of
18
+ # `ActiveRecord::Base` which declare `enum` fields
19
+ # (see https://api.rubyonrails.org/classes/ActiveRecord/Enum.html).
20
+ #
21
+ # For example, with the following `ActiveRecord::Base` subclass:
22
+ #
23
+ # ~~~rb
24
+ # class Post < ApplicationRecord
25
+ # enum title_type: %i(book all web), _suffix: :title
26
+ # end
27
+ # ~~~
28
+ #
29
+ # this generator will produce the RBI file `post.rbi` with the following content:
30
+ #
31
+ # ~~~rbi
32
+ # # post.rbi
33
+ # # typed: true
34
+ # class Post
35
+ # sig { void }
36
+ # def all_title!; end
37
+ #
38
+ # sig { returns(T::Boolean) }
39
+ # def all_title?; end
40
+ #
41
+ # sig { returns(T::Hash[T.any(String, Symbol), Integer]) }
42
+ # def self.title_types; end
43
+ #
44
+ # sig { void }
45
+ # def book_title!; end
46
+ #
47
+ # sig { returns(T::Boolean) }
48
+ # def book_title?; end
49
+ #
50
+ # sig { void }
51
+ # def web_title!; end
52
+ #
53
+ # sig { returns(T::Boolean) }
54
+ # def web_title?; end
55
+ # end
56
+ # ~~~
57
+ class ActiveRecordEnum < Base
58
+ extend T::Sig
59
+
60
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::ActiveRecord::Base)).void }
61
+ def decorate(root, constant)
62
+ return if constant.defined_enums.empty?
63
+
64
+ module_name = "#{constant}::EnumMethodsModule"
65
+ root.create_module(module_name) do |mod|
66
+ generate_instance_methods(constant, mod)
67
+ end
68
+
69
+ root.path(constant) do |k|
70
+ k.create_include(module_name)
71
+
72
+ constant.defined_enums.each do |name, enum_map|
73
+ type = type_for_enum(enum_map)
74
+ create_method(k, name.pluralize, class_method: true, return_type: type)
75
+ end
76
+ end
77
+ end
78
+
79
+ sig { override.returns(T::Enumerable[Module]) }
80
+ def gather_constants
81
+ ::ActiveRecord::Base.descendants.reject(&:abstract_class?)
82
+ end
83
+
84
+ private
85
+
86
+ sig { params(enum_map: T::Hash[T.untyped, T.untyped]).returns(String) }
87
+ def type_for_enum(enum_map)
88
+ value_type = enum_map.values.map { |v| v.class.name }.uniq
89
+ value_type = if value_type.length == 1
90
+ value_type.first
91
+ else
92
+ "T.any(#{value_type.join(', ')})"
93
+ end
94
+
95
+ "T::Hash[T.any(String, Symbol), #{value_type}]"
96
+ end
97
+
98
+ sig { params(constant: T.class_of(::ActiveRecord::Base), klass: Parlour::RbiGenerator::Namespace).void }
99
+ def generate_instance_methods(constant, klass)
100
+ methods = constant.send(:_enum_methods_module).instance_methods
101
+
102
+ methods.each do |method|
103
+ method = method.to_s
104
+ return_type = "T::Boolean" if method.end_with?("?")
105
+
106
+ create_method(klass, method, return_type: return_type)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,213 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "rails/railtie"
8
+ require "identity_cache"
9
+ rescue LoadError
10
+ # means IdentityCache is not installed,
11
+ # so let's not even define the generator.
12
+ return
13
+ end
14
+
15
+ module Tapioca
16
+ module Compilers
17
+ module Dsl
18
+ # `Tapioca::Compilers::DSL::ActiveRecordIdentityCache` generates RBI files for ActiveRecord models
19
+ # that use `include IdentityCache`
20
+ # `IdentityCache` is a blob level caching solution to plug into ActiveRecord. (see https://github.com/Shopify/identity_cache).
21
+ #
22
+ # For example, with the following ActiveRecord class:
23
+ #
24
+ # ~~~rb
25
+ # # post.rb
26
+ # class Post < ApplicationRecord
27
+ # include IdentityCache
28
+ #
29
+ # cache_index :blog_id
30
+ # cache_index :title, unique: true
31
+ # cache_index :title, :review_date, unique: true
32
+ #
33
+ # end
34
+ # ~~~
35
+ #
36
+ # this generator will produce the RBI file `post.rbi` with the following content:
37
+ #
38
+ # ~~~rbi
39
+ # # post.rbi
40
+ # # typed: true
41
+ # class Post
42
+ # sig { params(blog_id: T.untyped, includes: T.untyped).returns(T::Array[::Post])
43
+ # def fetch_by_blog_id(blog_id, includes: nil); end
44
+ #
45
+ # sig { params(blog_ids: T.untyped, includes: T.untyped).returns(T::Array[::Post])
46
+ # def fetch_multi_by_blog_id(index_values, includes: nil); end
47
+ #
48
+ # sig { params(title: T.untyped, includes: T.untyped).returns(::Post) }
49
+ # def fetch_by_title!(title, includes: nil); end
50
+ #
51
+ # sig { params(title: T.untyped, includes: T.untyped).returns(T.nilable(::Post)) }
52
+ # def fetch_by_title(title, includes: nil); end
53
+ #
54
+ # sig { params(index_values: T.untyped, includes: T.untyped).returns(T::Array[::Post]) }
55
+ # def fetch_multi_by_title(index_values, includes: nil); end
56
+ #
57
+ # sig { params(title: T.untyped, review_date: T.untyped, includes: T.untyped).returns(T::Array[::Post]) }
58
+ # def fetch_by_title_and_review_date!(title, review_date, includes: nil); end
59
+ #
60
+ # sig { params(title: T.untyped, review_date: T.untyped, includes: T.untyped).returns(T::Array[::Post]) }
61
+ # def fetch_by_title_and_review_date(title, review_date, includes: nil); end
62
+ # end
63
+ # ~~~
64
+
65
+ class ActiveRecordIdentityCache < Base
66
+ extend T::Sig
67
+
68
+ COLLECTION_TYPE = T.let(
69
+ ->(type) { "T::Array[::#{type}]" },
70
+ T.proc.params(type: Module).returns(String)
71
+ )
72
+
73
+ sig do
74
+ override
75
+ .params(
76
+ root: Parlour::RbiGenerator::Namespace,
77
+ constant: T.class_of(::ActiveRecord::Base)
78
+ )
79
+ .void
80
+ end
81
+ def decorate(root, constant)
82
+ caches = constant.send(:all_cached_associations)
83
+ cache_indexes = constant.send(:cache_indexes)
84
+ return if caches.empty? && cache_indexes.empty?
85
+
86
+ root.path(constant) do |k|
87
+ cache_manys = constant.send(:cached_has_manys)
88
+ cache_ones = constant.send(:cached_has_ones)
89
+ cache_belongs = constant.send(:cached_belongs_tos)
90
+
91
+ cache_indexes.each do |field|
92
+ create_fetch_by_methods(field, k, constant)
93
+ end
94
+
95
+ cache_manys.values.each do |field|
96
+ create_fetch_field_methods(field, k, returns_collection: true)
97
+ end
98
+
99
+ cache_ones.values.each do |field|
100
+ create_fetch_field_methods(field, k, returns_collection: false)
101
+ end
102
+
103
+ cache_belongs.values.each do |field|
104
+ create_fetch_field_methods(field, k, returns_collection: false)
105
+ end
106
+ end
107
+ end
108
+
109
+ sig { override.returns(T::Enumerable[Module]) }
110
+ def gather_constants
111
+ ::ActiveRecord::Base.descendants.select do |klass|
112
+ klass < IdentityCache
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ sig do
119
+ params(
120
+ field: T.untyped,
121
+ returns_collection: T::Boolean
122
+ )
123
+ .returns(String)
124
+ end
125
+ def type_for_field(field, returns_collection:)
126
+ cache_type = field.reflection.compute_class(field.reflection.class_name)
127
+ if returns_collection
128
+ COLLECTION_TYPE.call(cache_type)
129
+ else
130
+ "::#{cache_type}"
131
+ end
132
+ rescue ArgumentError
133
+ "T.untyped"
134
+ end
135
+
136
+ sig do
137
+ params(
138
+ field: T.untyped,
139
+ klass: Parlour::RbiGenerator::Namespace,
140
+ returns_collection: T::Boolean
141
+ )
142
+ .void
143
+ end
144
+ def create_fetch_field_methods(field, klass, returns_collection:)
145
+ name = field.cached_accessor_name.to_s
146
+ type = type_for_field(field, returns_collection: returns_collection)
147
+ klass.create_method(name, return_type: type)
148
+
149
+ if field.respond_to?(:cached_ids_name)
150
+ klass.create_method(field.cached_ids_name, return_type: "T::Array[T.untyped]")
151
+ elsif field.respond_to?(:cached_id_name)
152
+ klass.create_method(field.cached_id_name, return_type: "T.untyped")
153
+ end
154
+ end
155
+
156
+ sig do
157
+ params(
158
+ field: T.untyped,
159
+ klass: Parlour::RbiGenerator::Namespace,
160
+ constant: T.class_of(::ActiveRecord::Base),
161
+ )
162
+ .void
163
+ end
164
+ def create_fetch_by_methods(field, klass, constant)
165
+ field_length = field.key_fields.length
166
+ fields_name = field.key_fields.join("_and_")
167
+
168
+ parameters = field.key_fields.map do |arg|
169
+ Parlour::RbiGenerator::Parameter.new(arg.to_s, type: "T.untyped")
170
+ end
171
+ parameters << Parlour::RbiGenerator::Parameter.new("includes:", default: "nil", type: "T.untyped")
172
+
173
+ name = "fetch_by_#{fields_name}"
174
+ if field.unique
175
+ klass.create_method(
176
+ "#{name}!",
177
+ class_method: true,
178
+ parameters: parameters,
179
+ return_type: "::#{constant}"
180
+ )
181
+
182
+ klass.create_method(
183
+ name,
184
+ class_method: true,
185
+ parameters: parameters,
186
+ return_type: "T.nilable(::#{constant})"
187
+ )
188
+ else
189
+ klass.create_method(
190
+ name,
191
+ class_method: true,
192
+ parameters: parameters,
193
+ return_type: COLLECTION_TYPE.call(constant)
194
+ )
195
+ end
196
+
197
+ if field_length == 1
198
+ name = "fetch_multi_by_#{fields_name}"
199
+ klass.create_method(
200
+ name,
201
+ class_method: true,
202
+ parameters: [
203
+ Parlour::RbiGenerator::Parameter.new("index_values", type: "T.untyped"),
204
+ Parlour::RbiGenerator::Parameter.new("includes:", default: "nil", type: "T.untyped"),
205
+ ],
206
+ return_type: COLLECTION_TYPE.call(constant)
207
+ )
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,100 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "active_record"
8
+ rescue LoadError
9
+ return
10
+ end
11
+
12
+ module Tapioca
13
+ module Compilers
14
+ module Dsl
15
+ # `Tapioca::Compilers::Dsl::ActiveRecordScope` decorates RBI files for subclasses of
16
+ # `ActiveRecord::Base` which declare `scope` fields
17
+ # (see https://api.rubyonrails.org).
18
+ #
19
+ # For example, with the following `ActiveRecord::Base` subclass:
20
+ #
21
+ # ~~~rb
22
+ # class Post < ApplicationRecord
23
+ # scope :public_kind, -> { where.not(kind: 'private') }
24
+ # scope :private_kind, -> { where(kind: 'private') }
25
+ # end
26
+ # ~~~
27
+ #
28
+ # this generator will produce the RBI file `post.rbi` with the following content:
29
+ #
30
+ # ~~~rbi
31
+ # # post.rbi
32
+ # # typed: true
33
+ # class Post
34
+ # extend Post::GeneratedRelationMethods
35
+ # end
36
+ #
37
+ # module Post::GeneratedRelationMethods
38
+ # sig { params(args: T.untyped, blk: T.untyped).returns(T.untyped) }
39
+ # def private_kind(*args, &blk); end
40
+
41
+ # sig { params(args: T.untyped, blk: T.untyped).returns(T.untyped) }
42
+ # def public_kind(*args, &blk); end
43
+ # end
44
+ # ~~~
45
+ class ActiveRecordScope < Base
46
+ extend T::Sig
47
+
48
+ sig do
49
+ override.params(
50
+ root: Parlour::RbiGenerator::Namespace,
51
+ constant: T.class_of(::ActiveRecord::Base)
52
+ ).void
53
+ end
54
+ def decorate(root, constant)
55
+ scope_method_names = constant.send(:generated_relation_methods).instance_methods(false)
56
+ return if scope_method_names.empty?
57
+
58
+ module_name = "#{constant}::GeneratedRelationMethods"
59
+ root.create_module(module_name) do |mod|
60
+ scope_method_names.each do |scope_method|
61
+ generate_scope_method(scope_method, mod)
62
+ end
63
+ end
64
+
65
+ root.path(constant) do |k|
66
+ k.create_extend(module_name)
67
+ end
68
+ end
69
+
70
+ sig { override.returns(T::Enumerable[Module]) }
71
+ def gather_constants
72
+ ::ActiveRecord::Base.descendants.reject(&:abstract_class?)
73
+ end
74
+
75
+ private
76
+
77
+ sig do
78
+ params(
79
+ scope_method: String,
80
+ mod: Parlour::RbiGenerator::Namespace,
81
+ ).void
82
+ end
83
+ def generate_scope_method(scope_method, mod)
84
+ # This return type should actually be Model::ActiveRecord_Relation
85
+ return_type = "T.untyped"
86
+
87
+ create_method(
88
+ mod,
89
+ scope_method,
90
+ parameters: [
91
+ Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
92
+ Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
93
+ ],
94
+ return_type: return_type,
95
+ )
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,170 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+ require "tapioca/core_ext/class"
6
+
7
+ begin
8
+ require "activerecord-typedstore"
9
+ rescue LoadError
10
+ # means ActiveRecord::TypedStore is not installed,
11
+ # so let's not even define the generator.
12
+ return
13
+ end
14
+
15
+ module Tapioca
16
+ module Compilers
17
+ module Dsl
18
+ # `Tapioca::Compilers::DSL::ActiveRecordTypedStore` generates RBI files for ActiveRecord models that use
19
+ # `ActiveRecord::TypedStore` features (see https://github.com/byroot/activerecord-typedstore).
20
+ #
21
+ # For example, with the following ActiveRecord class:
22
+ #
23
+ # ~~~rb
24
+ # # post.rb
25
+ # class Post < ApplicationRecord
26
+ # typed_store :metadata do |s|
27
+ # s.string(:reviewer, blank: false, accessor: false)
28
+ # s.date(:review_date)
29
+ # s.boolean(:reviewed, null: false, default: false)
30
+ # end
31
+ # end
32
+ # ~~~
33
+ #
34
+ # this generator will produce the RBI file `post.rbi` with the following content:
35
+ #
36
+ # ~~~rbi
37
+ # # post.rbi
38
+ # # typed: true
39
+ # class Post
40
+ # sig { params(review_date: T.nilable(Date)).returns(T.nilable(Date)) }
41
+ # def review_date=(review_date); end
42
+ #
43
+ # sig { returns(T.nilable(Date)) }
44
+ # def review_date; end
45
+ #
46
+ # sig { returns(T.nilable(Date)) }
47
+ # def review_date_was; end
48
+ #
49
+ # sig { returns(T::Boolean) }
50
+ # def review_date_changed?; end
51
+ #
52
+ # sig { returns(T.nilable(Date)) }
53
+ # def review_date_before_last_save; end
54
+ #
55
+ # sig { returns(T::Boolean) }
56
+ # def saved_change_to_review_date?; end
57
+ #
58
+ # sig { returns(T.nilable([T.nilable(Date), T.nilable(Date)])) }
59
+ # def review_date_change; end
60
+ #
61
+ # sig { returns(T.nilable([T.nilable(Date), T.nilable(Date)])) }
62
+ # def saved_change_to_review_date; end
63
+ #
64
+ # sig { params(reviewd: T::Boolean).returns(T::Boolean) }
65
+ # def reviewed=(reviewed); end
66
+ #
67
+ # sig { returns(T::Boolean) }
68
+ # def reviewed; end
69
+ #
70
+ # sig { returns(T::Boolean) }
71
+ # def reviewed_was; end
72
+ #
73
+ # sig { returns(T::Boolean) }
74
+ # def reviewed_changed?; end
75
+ #
76
+ # sig { returns(T::Boolean) }
77
+ # def reviewed_before_last_save; end
78
+ #
79
+ # sig { returns(T::Boolean) }
80
+ # def saved_change_to_reviewed?; end
81
+ #
82
+ # sig { returns(T.nilable([T::Boolean, T::Boolean])) }
83
+ # def reviewed_change; end
84
+ #
85
+ # sig { returns(T.nilable([T::Boolean, T::Boolean])) }
86
+ # def saved_change_to_reviewed; end
87
+ # end
88
+ # ~~~
89
+ # end
90
+
91
+ class ActiveRecordTypedStore < Base
92
+ extend T::Sig
93
+
94
+ sig do
95
+ override
96
+ .params(
97
+ root: Parlour::RbiGenerator::Namespace,
98
+ constant: T.class_of(::ActiveRecord::Base)
99
+ )
100
+ .void
101
+ end
102
+ def decorate(root, constant)
103
+ stores = constant.typed_stores
104
+ return if stores.values.flat_map(&:accessors).empty?
105
+
106
+ root.path(constant) do |k|
107
+ stores.values.each do |store_data|
108
+ store_data.accessors.each do |accessor|
109
+ field = store_data.fields[accessor]
110
+ type = type_for(field.type_sym)
111
+ type = "T.nilable(#{type})" if field.null && type != "T.untyped"
112
+ generate_methods(field.name.to_s, type, k)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ sig { override.returns(T::Enumerable[Module]) }
119
+ def gather_constants
120
+ ::ActiveRecord::Base.descendants.select do |klass|
121
+ klass.include?(ActiveRecord::TypedStore::Behavior)
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ TYPES = T.let({
128
+ boolean: "T::Boolean",
129
+ integer: "Integer",
130
+ string: "String",
131
+ float: "Float",
132
+ date: "Date",
133
+ time: "Time",
134
+ datetime: "DateTime",
135
+ decimal: "BigDecimal",
136
+ any: "T.untyped",
137
+ }.freeze, T::Hash[Symbol, String])
138
+
139
+ sig { params(attr_type: Symbol).returns(String) }
140
+ def type_for(attr_type)
141
+ TYPES.fetch(attr_type, "T.untyped")
142
+ end
143
+
144
+ sig do
145
+ params(
146
+ name: String,
147
+ type: String,
148
+ klass: Parlour::RbiGenerator::Namespace
149
+ )
150
+ .void
151
+ end
152
+ def generate_methods(name, type, klass)
153
+ klass.create_method(
154
+ "#{name}=",
155
+ parameters: [Parlour::RbiGenerator::Parameter.new(name, type: type)],
156
+ return_type: type
157
+ )
158
+ klass.create_method(name, return_type: type)
159
+ klass.create_method("#{name}?", return_type: "T::Boolean")
160
+ klass.create_method("#{name}_was", return_type: type)
161
+ klass.create_method("#{name}_changed?", return_type: "T::Boolean")
162
+ klass.create_method("#{name}_before_last_save", return_type: type)
163
+ klass.create_method("saved_change_to_#{name}?", return_type: "T::Boolean")
164
+ klass.create_method("#{name}_change", return_type: "T.nilable([#{type}, #{type}])")
165
+ klass.create_method("saved_change_to_#{name}", return_type: "T.nilable([#{type}, #{type}])")
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end