tapioca 0.4.27 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +15 -15
  3. data/README.md +2 -2
  4. data/Rakefile +5 -7
  5. data/exe/tapioca +2 -2
  6. data/lib/tapioca/cli.rb +172 -2
  7. data/lib/tapioca/compilers/dsl/aasm.rb +122 -0
  8. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +52 -12
  9. data/lib/tapioca/compilers/dsl/action_mailer.rb +6 -9
  10. data/lib/tapioca/compilers/dsl/active_job.rb +8 -12
  11. data/lib/tapioca/compilers/dsl/active_model_attributes.rb +131 -0
  12. data/lib/tapioca/compilers/dsl/active_model_secure_password.rb +101 -0
  13. data/lib/tapioca/compilers/dsl/active_record_associations.rb +33 -54
  14. data/lib/tapioca/compilers/dsl/active_record_columns.rb +10 -105
  15. data/lib/tapioca/compilers/dsl/active_record_enum.rb +8 -10
  16. data/lib/tapioca/compilers/dsl/active_record_fixtures.rb +86 -0
  17. data/lib/tapioca/compilers/dsl/active_record_scope.rb +7 -10
  18. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +5 -8
  19. data/lib/tapioca/compilers/dsl/active_resource.rb +9 -37
  20. data/lib/tapioca/compilers/dsl/active_storage.rb +98 -0
  21. data/lib/tapioca/compilers/dsl/active_support_concern.rb +106 -0
  22. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +13 -8
  23. data/lib/tapioca/compilers/dsl/base.rb +108 -82
  24. data/lib/tapioca/compilers/dsl/config.rb +111 -0
  25. data/lib/tapioca/compilers/dsl/frozen_record.rb +5 -7
  26. data/lib/tapioca/compilers/dsl/identity_cache.rb +66 -29
  27. data/lib/tapioca/compilers/dsl/mixed_in_class_attributes.rb +74 -0
  28. data/lib/tapioca/compilers/dsl/protobuf.rb +19 -69
  29. data/lib/tapioca/compilers/dsl/sidekiq_worker.rb +25 -12
  30. data/lib/tapioca/compilers/dsl/smart_properties.rb +21 -33
  31. data/lib/tapioca/compilers/dsl/state_machines.rb +56 -78
  32. data/lib/tapioca/compilers/dsl/url_helpers.rb +7 -10
  33. data/lib/tapioca/compilers/dsl_compiler.rb +25 -40
  34. data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +198 -0
  35. data/lib/tapioca/compilers/requires_compiler.rb +2 -2
  36. data/lib/tapioca/compilers/sorbet.rb +25 -5
  37. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +122 -206
  38. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +4 -4
  39. data/lib/tapioca/compilers/symbol_table_compiler.rb +5 -11
  40. data/lib/tapioca/compilers/todos_compiler.rb +1 -1
  41. data/lib/tapioca/config.rb +3 -0
  42. data/lib/tapioca/config_builder.rb +5 -2
  43. data/lib/tapioca/constant_locator.rb +6 -8
  44. data/lib/tapioca/gemfile.rb +14 -11
  45. data/lib/tapioca/generators/base.rb +61 -0
  46. data/lib/tapioca/generators/dsl.rb +362 -0
  47. data/lib/tapioca/generators/gem.rb +345 -0
  48. data/lib/tapioca/generators/init.rb +79 -0
  49. data/lib/tapioca/generators/require.rb +52 -0
  50. data/lib/tapioca/generators/todo.rb +76 -0
  51. data/lib/tapioca/generators.rb +9 -0
  52. data/lib/tapioca/generic_type_registry.rb +25 -98
  53. data/lib/tapioca/helpers/active_record_column_type_helper.rb +98 -0
  54. data/lib/tapioca/internal.rb +2 -10
  55. data/lib/tapioca/loader.rb +11 -31
  56. data/lib/tapioca/rbi_ext/model.rb +166 -0
  57. data/lib/tapioca/reflection.rb +138 -0
  58. data/lib/tapioca/sorbet_ext/fixed_hash_patch.rb +1 -1
  59. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +72 -4
  60. data/lib/tapioca/sorbet_ext/name_patch.rb +1 -1
  61. data/lib/tapioca/version.rb +1 -1
  62. data/lib/tapioca.rb +3 -0
  63. metadata +45 -23
  64. data/lib/tapioca/cli/main.rb +0 -146
  65. data/lib/tapioca/core_ext/class.rb +0 -28
  66. data/lib/tapioca/core_ext/string.rb +0 -18
  67. data/lib/tapioca/generator.rb +0 -633
  68. data/lib/tapioca/rbi/model.rb +0 -405
  69. data/lib/tapioca/rbi/printer.rb +0 -410
  70. data/lib/tapioca/rbi/rewriters/group_nodes.rb +0 -106
  71. data/lib/tapioca/rbi/rewriters/nest_non_public_methods.rb +0 -65
  72. data/lib/tapioca/rbi/rewriters/nest_singleton_methods.rb +0 -42
  73. data/lib/tapioca/rbi/rewriters/sort_nodes.rb +0 -86
  74. data/lib/tapioca/rbi/visitor.rb +0 -21
@@ -0,0 +1,345 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Generators
6
+ class Gem < Base
7
+ sig do
8
+ params(
9
+ gem_names: T::Array[String],
10
+ gem_excludes: T::Array[String],
11
+ prerequire: T.nilable(String),
12
+ postrequire: String,
13
+ typed_overrides: T::Hash[String, String],
14
+ default_command: String,
15
+ outpath: Pathname,
16
+ file_header: T::Boolean,
17
+ doc: T::Boolean,
18
+ file_writer: Thor::Actions
19
+ ).void
20
+ end
21
+ def initialize(
22
+ gem_names:,
23
+ gem_excludes:,
24
+ prerequire:,
25
+ postrequire:,
26
+ typed_overrides:,
27
+ default_command:,
28
+ outpath:,
29
+ file_header:,
30
+ doc:,
31
+ file_writer: FileWriter.new
32
+ )
33
+ @gem_names = gem_names
34
+ @gem_excludes = gem_excludes
35
+ @prerequire = prerequire
36
+ @postrequire = postrequire
37
+ @typed_overrides = typed_overrides
38
+ @outpath = outpath
39
+ @file_header = file_header
40
+
41
+ super(default_command: default_command, file_writer: file_writer)
42
+
43
+ @loader = T.let(nil, T.nilable(Loader))
44
+ @bundle = T.let(nil, T.nilable(Gemfile))
45
+ @existing_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
46
+ @expected_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
47
+ @doc = T.let(doc, T::Boolean)
48
+ end
49
+
50
+ sig { override.void }
51
+ def generate
52
+ require_gem_file
53
+
54
+ gems_to_generate(@gem_names)
55
+ .reject { |gem| @gem_excludes.include?(gem.name) }
56
+ .each do |gem|
57
+ say("Processing '#{gem.name}' gem:", :green)
58
+ shell.indent do
59
+ compile_gem_rbi(gem)
60
+ puts
61
+ end
62
+ end
63
+
64
+ say("All operations performed in working directory.", [:green, :bold])
65
+ say("Please review changes and commit them.", [:green, :bold])
66
+ end
67
+
68
+ sig { params(should_verify: T::Boolean).void }
69
+ def sync(should_verify: false)
70
+ if should_verify
71
+ say("Checking for out-of-date RBIs...")
72
+ say("")
73
+ perform_sync_verification
74
+ return
75
+ end
76
+
77
+ anything_done = [
78
+ perform_removals,
79
+ perform_additions,
80
+ ].any?
81
+
82
+ if anything_done
83
+ say("All operations performed in working directory.", [:green, :bold])
84
+ say("Please review changes and commit them.", [:green, :bold])
85
+ else
86
+ say("No operations performed, all RBIs are up-to-date.", [:green, :bold])
87
+ end
88
+
89
+ puts
90
+ end
91
+
92
+ private
93
+
94
+ sig { returns(Loader) }
95
+ def loader
96
+ @loader ||= Loader.new
97
+ end
98
+
99
+ sig { returns(Gemfile) }
100
+ def bundle
101
+ @bundle ||= Gemfile.new
102
+ end
103
+
104
+ sig { void }
105
+ def require_gem_file
106
+ say("Requiring all gems to prepare for compiling... ")
107
+ begin
108
+ loader.load_bundle(bundle, @prerequire, @postrequire)
109
+ rescue LoadError => e
110
+ explain_failed_require(@postrequire, e)
111
+ exit(1)
112
+ end
113
+ say(" Done", :green)
114
+ unless bundle.missing_specs.empty?
115
+ say(" completed with missing specs: ")
116
+ say(bundle.missing_specs.join(", "), :yellow)
117
+ end
118
+ puts
119
+ end
120
+
121
+ sig { params(gem_names: T::Array[String]).returns(T::Array[Gemfile::GemSpec]) }
122
+ def gems_to_generate(gem_names)
123
+ return bundle.dependencies if gem_names.empty?
124
+
125
+ gem_names.map do |gem_name|
126
+ gem = bundle.gem(gem_name)
127
+ if gem.nil?
128
+ say("Error: Cannot find gem '#{gem_name}'", :red)
129
+ exit(1)
130
+ end
131
+ gem
132
+ end
133
+ end
134
+
135
+ sig { params(gem: Gemfile::GemSpec).void }
136
+ def compile_gem_rbi(gem)
137
+ gem_name = set_color(gem.name, :yellow, :bold)
138
+ say("Compiling #{gem_name}, this may take a few seconds... ")
139
+
140
+ rbi = RBI::File.new(strictness: @typed_overrides[gem.name] || "true")
141
+ rbi.set_file_header(
142
+ "#{@default_command} gem #{gem.name}",
143
+ reason: "types exported from the `#{gem.name}` gem",
144
+ display_heading: @file_header
145
+ )
146
+
147
+ Compilers::SymbolTableCompiler.new.compile(gem, rbi, 0, @doc)
148
+
149
+ if rbi.empty?
150
+ rbi.set_empty_body_content
151
+ say("Done (empty output)", :yellow)
152
+ else
153
+ say("Done", :green)
154
+ end
155
+
156
+ create_file(@outpath / gem.rbi_file_name, rbi.transformed_string)
157
+
158
+ T.unsafe(Pathname).glob((@outpath / "#{gem.name}@*.rbi").to_s) do |file|
159
+ remove(file) unless file.basename.to_s == gem.rbi_file_name
160
+ end
161
+ end
162
+
163
+ sig { void }
164
+ def perform_sync_verification
165
+ diff = {}
166
+
167
+ removed_rbis.each do |gem_name|
168
+ filename = existing_rbi(gem_name)
169
+ diff[filename] = :removed
170
+ end
171
+
172
+ added_rbis.each do |gem_name|
173
+ filename = expected_rbi(gem_name)
174
+ diff[filename] = gem_rbi_exists?(gem_name) ? :changed : :added
175
+ end
176
+
177
+ report_diff_and_exit_if_out_of_date(diff, "gem")
178
+ end
179
+
180
+ sig { void }
181
+ def perform_removals
182
+ say("Removing RBI files of gems that have been removed:", [:blue, :bold])
183
+ puts
184
+
185
+ anything_done = T.let(false, T::Boolean)
186
+
187
+ gems = removed_rbis
188
+
189
+ shell.indent do
190
+ if gems.empty?
191
+ say("Nothing to do.")
192
+ else
193
+ gems.each do |removed|
194
+ filename = existing_rbi(removed)
195
+ remove(filename)
196
+ end
197
+
198
+ anything_done = true
199
+ end
200
+ end
201
+
202
+ puts
203
+
204
+ anything_done
205
+ end
206
+
207
+ sig { void }
208
+ def perform_additions
209
+ say("Generating RBI files of gems that are added or updated:", [:blue, :bold])
210
+ puts
211
+
212
+ anything_done = T.let(false, T::Boolean)
213
+
214
+ gems = added_rbis
215
+
216
+ shell.indent do
217
+ if gems.empty?
218
+ say("Nothing to do.")
219
+ else
220
+ require_gem_file
221
+
222
+ gems.each do |gem_name|
223
+ filename = expected_rbi(gem_name)
224
+
225
+ if gem_rbi_exists?(gem_name)
226
+ old_filename = existing_rbi(gem_name)
227
+ move(old_filename, filename) unless old_filename == filename
228
+ end
229
+
230
+ gem = T.must(bundle.gem(gem_name))
231
+ compile_gem_rbi(gem)
232
+ puts
233
+ end
234
+ end
235
+
236
+ anything_done = true
237
+ end
238
+
239
+ puts
240
+
241
+ anything_done
242
+ end
243
+
244
+ sig { params(file: String, error: LoadError).void }
245
+ def explain_failed_require(file, error)
246
+ say_error("\n\nLoadError: #{error}", :bold, :red)
247
+ say_error("\nTapioca could not load all the gems required by your application.", :yellow)
248
+ say_error("If you populated ", :yellow)
249
+ say_error("#{file} ", :bold, :blue)
250
+ say_error("with ", :yellow)
251
+ say_error("`#{@default_command} require`", :bold, :blue)
252
+ say_error("you should probably review it and remove the faulty line.", :yellow)
253
+ end
254
+
255
+ sig { params(filename: Pathname).void }
256
+ def remove(filename)
257
+ return unless filename.exist?
258
+ say("-- Removing: #{filename}")
259
+ filename.unlink
260
+ end
261
+
262
+ sig { returns(T::Array[String]) }
263
+ def removed_rbis
264
+ (existing_rbis.keys - expected_rbis.keys).sort
265
+ end
266
+
267
+ sig { params(gem_name: String).returns(Pathname) }
268
+ def existing_rbi(gem_name)
269
+ gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
270
+ end
271
+
272
+ sig { returns(T::Array[String]) }
273
+ def added_rbis
274
+ expected_rbis.select do |name, value|
275
+ existing_rbis[name] != value
276
+ end.keys.sort
277
+ end
278
+
279
+ sig { params(gem_name: String).returns(Pathname) }
280
+ def expected_rbi(gem_name)
281
+ gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
282
+ end
283
+
284
+ sig { params(gem_name: String).returns(T::Boolean) }
285
+ def gem_rbi_exists?(gem_name)
286
+ existing_rbis.key?(gem_name)
287
+ end
288
+
289
+ sig { params(diff: T::Hash[String, Symbol], command: String).void }
290
+ def report_diff_and_exit_if_out_of_date(diff, command)
291
+ if diff.empty?
292
+ say("Nothing to do, all RBIs are up-to-date.")
293
+ else
294
+ say("RBI files are out-of-date. In your development environment, please run:", :green)
295
+ say(" `#{Config::DEFAULT_COMMAND} #{command}`", [:green, :bold])
296
+ say("Once it is complete, be sure to commit and push any changes", :green)
297
+
298
+ say("")
299
+
300
+ say("Reason:", [:red])
301
+ diff.group_by(&:last).sort.each do |cause, diff_for_cause|
302
+ say(build_error_for_files(cause, diff_for_cause.map(&:first)))
303
+ end
304
+
305
+ exit(1)
306
+ end
307
+ end
308
+
309
+ sig { params(old_filename: Pathname, new_filename: Pathname).void }
310
+ def move(old_filename, new_filename)
311
+ say("-> Moving: #{old_filename} to #{new_filename}")
312
+ old_filename.rename(new_filename.to_s)
313
+ end
314
+
315
+ sig { returns(T::Hash[String, String]) }
316
+ def existing_rbis
317
+ @existing_rbis ||= Pathname.glob((@outpath / "*@*.rbi").to_s)
318
+ .map { |f| T.cast(f.basename(".*").to_s.split("@", 2), [String, String]) }
319
+ .to_h
320
+ end
321
+
322
+ sig { returns(T::Hash[String, String]) }
323
+ def expected_rbis
324
+ @expected_rbis ||= bundle.dependencies
325
+ .reject { |gem| @gem_excludes.include?(gem.name) }
326
+ .map { |gem| [gem.name, gem.version.to_s] }
327
+ .to_h
328
+ end
329
+
330
+ sig { params(gem_name: String, version: String).returns(Pathname) }
331
+ def gem_rbi_filename(gem_name, version)
332
+ @outpath / "#{gem_name}@#{version}.rbi"
333
+ end
334
+
335
+ sig { params(cause: Symbol, files: T::Array[String]).returns(String) }
336
+ def build_error_for_files(cause, files)
337
+ filenames = files.map do |file|
338
+ @outpath / file
339
+ end.join("\n - ")
340
+
341
+ " File(s) #{cause}:\n - #{filenames}"
342
+ end
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,79 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Generators
6
+ class Init < Base
7
+ sig do
8
+ params(
9
+ sorbet_config: String,
10
+ default_postrequire: String,
11
+ default_command: String,
12
+ file_writer: Thor::Actions
13
+ ).void
14
+ end
15
+ def initialize(sorbet_config:, default_postrequire:, default_command:, file_writer: FileWriter.new)
16
+ @sorbet_config = sorbet_config
17
+ @default_postrequire = default_postrequire
18
+
19
+ super(default_command: default_command, file_writer: file_writer)
20
+
21
+ @installer = T.let(nil, T.nilable(Bundler::Installer))
22
+ @spec = T.let(nil, T.nilable(Bundler::StubSpecification))
23
+ end
24
+
25
+ sig { override.void }
26
+ def generate
27
+ create_config
28
+ create_post_require
29
+ if File.exist?(@default_command)
30
+ generate_binstub!
31
+ else
32
+ generate_binstub
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ sig { void }
39
+ def create_config
40
+ create_file(@sorbet_config, <<~CONTENT, skip: true, force: false)
41
+ --dir
42
+ .
43
+ CONTENT
44
+ end
45
+
46
+ sig { void }
47
+ def create_post_require
48
+ create_file(@default_postrequire, <<~CONTENT, skip: true, force: false)
49
+ # typed: true
50
+ # frozen_string_literal: true
51
+
52
+ # Add your extra requires here (`#{@default_command} require` can be used to boostrap this list)
53
+ CONTENT
54
+ end
55
+
56
+ sig { void }
57
+ def generate_binstub!
58
+ installer.generate_bundler_executable_stubs(spec, { force: true })
59
+ say_status(:force, @default_command, :yellow)
60
+ end
61
+
62
+ sig { void }
63
+ def generate_binstub
64
+ installer.generate_bundler_executable_stubs(spec)
65
+ say_status(:create, @default_command, :green)
66
+ end
67
+
68
+ sig { returns(Bundler::Installer) }
69
+ def installer
70
+ @installer ||= Bundler::Installer.new(Bundler.root, Bundler.definition)
71
+ end
72
+
73
+ sig { returns(Bundler::StubSpecification) }
74
+ def spec
75
+ @spec ||= Bundler.definition.specs.find { |s| s.name == "tapioca" }
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,52 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Generators
6
+ class Require < Base
7
+ sig do
8
+ params(
9
+ requires_path: String,
10
+ sorbet_config_path: String,
11
+ default_command: String,
12
+ file_writer: Thor::Actions
13
+ ).void
14
+ end
15
+ def initialize(requires_path:, sorbet_config_path:, default_command:, file_writer: FileWriter.new)
16
+ @requires_path = requires_path
17
+ @sorbet_config_path = sorbet_config_path
18
+
19
+ super(default_command: default_command, file_writer: file_writer)
20
+ end
21
+
22
+ sig { override.void }
23
+ def generate
24
+ compiler = Compilers::RequiresCompiler.new(@sorbet_config_path)
25
+ name = set_color(@requires_path, :yellow, :bold)
26
+ say("Compiling #{name}, this may take a few seconds... ")
27
+
28
+ rb_string = compiler.compile
29
+ if rb_string.empty?
30
+ say("Nothing to do", :green)
31
+ return
32
+ end
33
+
34
+ # Clean all existing requires before regenerating the list so we update
35
+ # it with the new one found in the client code and remove the old ones.
36
+ File.delete(@requires_path) if File.exist?(@requires_path)
37
+
38
+ content = +"# typed: true\n"
39
+ content << "# frozen_string_literal: true\n\n"
40
+ content << rb_string
41
+
42
+ create_file(@requires_path, content, verbose: false)
43
+
44
+ say("Done", :green)
45
+
46
+ say("All requires from this application have been written to #{name}.", [:green, :bold])
47
+ cmd = set_color("#{@default_command} gem", :yellow, :bold)
48
+ say("Please review changes and commit them, then run `#{cmd}`.", [:green, :bold])
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,76 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Generators
6
+ class Todo < Base
7
+ sig do
8
+ params(
9
+ todos_path: String,
10
+ file_header: T::Boolean,
11
+ default_command: String,
12
+ file_writer: Thor::Actions
13
+ ).void
14
+ end
15
+ def initialize(todos_path:, file_header:, default_command:, file_writer: FileWriter.new)
16
+ @todos_path = todos_path
17
+ @file_header = file_header
18
+
19
+ super(default_command: default_command, file_writer: file_writer)
20
+ end
21
+
22
+ sig { override.void }
23
+ def generate
24
+ compiler = Compilers::TodosCompiler.new
25
+ say("Finding all unresolved constants, this may take a few seconds... ")
26
+
27
+ # Clean all existing unresolved constants before regenerating the list
28
+ # so Sorbet won't grab them as already resolved.
29
+ File.delete(@todos_path) if File.exist?(@todos_path)
30
+
31
+ rbi_string = compiler.compile
32
+ if rbi_string.empty?
33
+ say("Nothing to do", :green)
34
+ return
35
+ end
36
+
37
+ content = String.new
38
+ content << rbi_header(
39
+ "#{@default_command} todo",
40
+ reason: "unresolved constants",
41
+ strictness: "false"
42
+ )
43
+ content << rbi_string
44
+ content << "\n"
45
+
46
+ say("Done", :green)
47
+ create_file(@todos_path, content, verbose: false)
48
+
49
+ name = set_color(@todos_path, :yellow, :bold)
50
+ say("\nAll unresolved constants have been written to #{name}.", [:green, :bold])
51
+ say("Please review changes and commit them.", [:green, :bold])
52
+ end
53
+
54
+ sig { params(command: String, reason: T.nilable(String), strictness: T.nilable(String)).returns(String) }
55
+ def rbi_header(command, reason: nil, strictness: nil)
56
+ statement = <<~HEAD
57
+ # DO NOT EDIT MANUALLY
58
+ # This is an autogenerated file for #{reason}.
59
+ # Please instead update this file by running `#{command}`.
60
+ HEAD
61
+
62
+ sigil = <<~SIGIL if strictness
63
+ # typed: #{strictness}
64
+ SIGIL
65
+
66
+ if @file_header
67
+ [statement, sigil].compact.join("\n").strip.concat("\n\n")
68
+ elsif sigil
69
+ sigil.strip.concat("\n\n")
70
+ else
71
+ ""
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,9 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "generators/base"
5
+ require_relative "generators/dsl"
6
+ require_relative "generators/init"
7
+ require_relative "generators/gem"
8
+ require_relative "generators/require"
9
+ require_relative "generators/todo"