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
@@ -2,6 +2,8 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "bundler"
5
+ require "logger"
6
+ require "yard-sorbet"
5
7
 
6
8
  module Tapioca
7
9
  class Gemfile
@@ -9,10 +11,7 @@ module Tapioca
9
11
 
10
12
  Spec = T.type_alias do
11
13
  T.any(
12
- T.all(
13
- ::Bundler::StubSpecification,
14
- ::Bundler::RemoteSpecification
15
- ),
14
+ ::Bundler::StubSpecification,
16
15
  ::Gem::Specification
17
16
  )
18
17
  end
@@ -62,7 +61,7 @@ module Tapioca
62
61
  [dependencies, missing_specs]
63
62
  end
64
63
 
65
- sig { returns([T::Array[::Gem::Specification], T::Array[String]]) }
64
+ sig { returns([T::Enumerable[Spec], T::Array[String]]) }
66
65
  def materialize_deps
67
66
  deps = definition.locked_gems.dependencies.values
68
67
  missing_specs = T::Array[String].new
@@ -95,9 +94,7 @@ module Tapioca
95
94
  class GemSpec
96
95
  extend(T::Sig)
97
96
 
98
- IGNORED_GEMS = T.let(%w{
99
- sorbet sorbet-static sorbet-runtime
100
- }.freeze, T::Array[String])
97
+ IGNORED_GEMS = T.let(["sorbet", "sorbet-static", "sorbet-runtime"].freeze, T::Array[String])
101
98
 
102
99
  sig { returns(String) }
103
100
  attr_reader :full_gem_path, :version
@@ -117,12 +114,13 @@ module Tapioca
117
114
 
118
115
  sig { returns(T::Array[Pathname]) }
119
116
  def files
120
- if default_gem?
121
- @spec.files.map do |file|
117
+ spec = @spec
118
+ if default_gem? && spec.is_a?(::Gem::Specification)
119
+ spec.files.map do |file|
122
120
  ruby_lib_dir.join(file)
123
121
  end
124
122
  else
125
- @spec.full_require_paths.flat_map do |path|
123
+ spec.full_require_paths.flat_map do |path|
126
124
  Pathname.glob((Pathname.new(path) / "**/*.rb").to_s)
127
125
  end
128
126
  end
@@ -147,6 +145,11 @@ module Tapioca
147
145
  end
148
146
  end
149
147
 
148
+ sig { void }
149
+ def parse_yard_docs
150
+ files.each { |path| YARD.parse(path.to_s, [], Logger::Severity::FATAL) }
151
+ end
152
+
150
153
  private
151
154
 
152
155
  sig { returns(T::Boolean) }
@@ -0,0 +1,61 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # TODO: Remove me when logging logic has been abstracted.
5
+ require "thor"
6
+
7
+ module Tapioca
8
+ module Generators
9
+ class Base
10
+ extend T::Sig
11
+ extend T::Helpers
12
+
13
+ class FileWriter < Thor
14
+ include Thor::Actions
15
+ end
16
+
17
+ # TODO: Remove me when logging logic has been abstracted
18
+ include Thor::Base
19
+
20
+ abstract!
21
+
22
+ sig { params(default_command: String, file_writer: Thor::Actions).void }
23
+ def initialize(default_command:, file_writer: FileWriter.new)
24
+ @file_writer = file_writer
25
+ @default_command = default_command
26
+ end
27
+
28
+ sig { abstract.void }
29
+ def generate; end
30
+
31
+ private
32
+
33
+ # TODO: Remove me when logging logic has been abstracted
34
+ sig { params(message: String, color: T.any(Symbol, T::Array[Symbol])).void }
35
+ def say_error(message = "", *color)
36
+ force_new_line = (message.to_s !~ /( |\t)\Z/)
37
+ # NOTE: This is a hack. We're no longer subclassing from Thor::Shell::Color
38
+ # so we no longer have access to the prepare_message call.
39
+ # We should update this to remove this.
40
+ buffer = shell.send(:prepare_message, *T.unsafe([message, *T.unsafe(color)]))
41
+ buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
42
+
43
+ $stderr.print(buffer)
44
+ $stderr.flush
45
+ end
46
+
47
+ sig do
48
+ params(
49
+ path: T.any(String, Pathname),
50
+ content: String,
51
+ force: T::Boolean,
52
+ skip: T::Boolean,
53
+ verbose: T::Boolean
54
+ ).void
55
+ end
56
+ def create_file(path, content, force: true, skip: false, verbose: true)
57
+ @file_writer.create_file(path, force: force, skip: skip, verbose: verbose) { content }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,362 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Generators
6
+ class Dsl < Base
7
+ sig do
8
+ params(
9
+ requested_constants: T::Array[String],
10
+ outpath: Pathname,
11
+ generators: T::Array[String],
12
+ exclude_generators: T::Array[String],
13
+ file_header: T::Boolean,
14
+ compiler_path: String,
15
+ tapioca_path: String,
16
+ default_command: String,
17
+ file_writer: Thor::Actions,
18
+ should_verify: T::Boolean,
19
+ quiet: T::Boolean,
20
+ verbose: T::Boolean
21
+ ).void
22
+ end
23
+ def initialize(
24
+ requested_constants:,
25
+ outpath:,
26
+ generators:,
27
+ exclude_generators:,
28
+ file_header:,
29
+ compiler_path:,
30
+ tapioca_path:,
31
+ default_command:,
32
+ file_writer: FileWriter.new,
33
+ should_verify: false,
34
+ quiet: false,
35
+ verbose: false
36
+ )
37
+ @requested_constants = requested_constants
38
+ @outpath = outpath
39
+ @generators = generators
40
+ @exclude_generators = exclude_generators
41
+ @file_header = file_header
42
+ @compiler_path = compiler_path
43
+ @tapioca_path = tapioca_path
44
+ @should_verify = should_verify
45
+ @quiet = quiet
46
+ @verbose = verbose
47
+
48
+ super(default_command: default_command, file_writer: file_writer)
49
+
50
+ @loader = T.let(nil, T.nilable(Loader))
51
+ end
52
+
53
+ sig { override.void }
54
+ def generate
55
+ load_application(eager_load: @requested_constants.empty?)
56
+ abort_if_pending_migrations!
57
+ load_dsl_generators
58
+
59
+ if @should_verify
60
+ say("Checking for out-of-date RBIs...")
61
+ else
62
+ say("Compiling DSL RBI files...")
63
+ end
64
+ say("")
65
+
66
+ outpath = @should_verify ? Pathname.new(Dir.mktmpdir) : @outpath
67
+ rbi_files_to_purge = existing_rbi_filenames(@requested_constants)
68
+
69
+ compiler = Compilers::DslCompiler.new(
70
+ requested_constants: constantize(@requested_constants),
71
+ requested_generators: constantize_generators(@generators),
72
+ excluded_generators: constantize_generators(@exclude_generators),
73
+ error_handler: ->(error) {
74
+ say_error(error, :bold, :red)
75
+ }
76
+ )
77
+
78
+ compiler.run do |constant, contents|
79
+ constant_name = T.must(Reflection.name_of(constant))
80
+
81
+ if @verbose && !@quiet
82
+ say_status(:processing, constant_name, :yellow)
83
+ end
84
+
85
+ filename = compile_dsl_rbi(
86
+ constant_name,
87
+ contents,
88
+ outpath: outpath,
89
+ quiet: @should_verify || @quiet && !@verbose
90
+ )
91
+
92
+ if filename
93
+ rbi_files_to_purge.delete(filename)
94
+ end
95
+ end
96
+ say("")
97
+
98
+ if @should_verify
99
+ perform_dsl_verification(outpath)
100
+ else
101
+ purge_stale_dsl_rbi_files(rbi_files_to_purge)
102
+
103
+ say("Done", :green)
104
+
105
+ say("All operations performed in working directory.", [:green, :bold])
106
+ say("Please review changes and commit them.", [:green, :bold])
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ sig { params(eager_load: T::Boolean).void }
113
+ def load_application(eager_load:)
114
+ say("Loading Rails application... ")
115
+
116
+ loader.load_rails_application(
117
+ environment_load: true,
118
+ eager_load: eager_load
119
+ )
120
+
121
+ say("Done", :green)
122
+ end
123
+
124
+ sig { void }
125
+ def abort_if_pending_migrations!
126
+ return unless File.exist?("config/application.rb")
127
+ return unless defined?(::Rake)
128
+
129
+ Rails.application.load_tasks
130
+ if Rake::Task.task_defined?("db:abort_if_pending_migrations")
131
+ Rake::Task["db:abort_if_pending_migrations"].invoke
132
+ end
133
+ end
134
+
135
+ sig { void }
136
+ def load_dsl_generators
137
+ say("Loading DSL generator classes... ")
138
+
139
+ Dir.glob([
140
+ "#{@compiler_path}/*.rb",
141
+ "#{@tapioca_path}/generators/**/*.rb",
142
+ ]).each do |generator|
143
+ require File.expand_path(generator)
144
+ end
145
+
146
+ say("Done", :green)
147
+ end
148
+
149
+ sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) }
150
+ def existing_rbi_filenames(requested_constants, path: @outpath)
151
+ filenames = if requested_constants.empty?
152
+ Pathname.glob(path / "**/*.rbi")
153
+ else
154
+ requested_constants.map do |constant_name|
155
+ dsl_rbi_filename(constant_name)
156
+ end
157
+ end
158
+
159
+ filenames.to_set
160
+ end
161
+
162
+ sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
163
+ def constantize(constant_names)
164
+ constant_map = constant_names.map do |name|
165
+ [name, Object.const_get(name)]
166
+ rescue NameError
167
+ [name, nil]
168
+ end.to_h
169
+
170
+ unprocessable_constants = constant_map.select { |_, v| v.nil? }
171
+ unless unprocessable_constants.empty?
172
+ unprocessable_constants.each do |name, _|
173
+ say("Error: Cannot find constant '#{name}'", :red)
174
+ remove(dsl_rbi_filename(name))
175
+ end
176
+
177
+ exit(1)
178
+ end
179
+
180
+ constant_map.values
181
+ end
182
+
183
+ sig { params(generator_names: T::Array[String]).returns(T::Array[T.class_of(Compilers::Dsl::Base)]) }
184
+ def constantize_generators(generator_names)
185
+ generator_map = generator_names.map do |name|
186
+ # Try to find built-in tapioca generator first, then globally defined generator. The
187
+ # explicit `break` ensures the class is returned, not the `potential_name`.
188
+ generator_klass = ["Tapioca::Compilers::Dsl::#{name}", name].find do |potential_name|
189
+ break Object.const_get(potential_name)
190
+ rescue NameError
191
+ # Skip if we can't find generator by the potential name
192
+ end
193
+
194
+ [name, generator_klass]
195
+ end.to_h
196
+
197
+ unprocessable_generators = generator_map.select { |_, v| v.nil? }
198
+ unless unprocessable_generators.empty?
199
+ unprocessable_generators.each do |name, _|
200
+ say("Error: Cannot find generator '#{name}'", :red)
201
+ end
202
+
203
+ exit(1)
204
+ end
205
+
206
+ generator_map.values
207
+ end
208
+
209
+ sig do
210
+ params(
211
+ constant_name: String,
212
+ rbi: RBI::File,
213
+ outpath: Pathname,
214
+ quiet: T::Boolean
215
+ ).returns(T.nilable(Pathname))
216
+ end
217
+ def compile_dsl_rbi(constant_name, rbi, outpath: @outpath, quiet: false)
218
+ return if rbi.empty?
219
+
220
+ filename = outpath / rbi_filename_for(constant_name)
221
+
222
+ rbi.set_file_header(
223
+ generate_command_for(constant_name),
224
+ reason: "dynamic methods in `#{constant_name}`",
225
+ display_heading: @file_header
226
+ )
227
+
228
+ create_file(filename, rbi.transformed_string, verbose: !quiet)
229
+
230
+ filename
231
+ end
232
+
233
+ sig { params(dir: Pathname).void }
234
+ def perform_dsl_verification(dir)
235
+ diff = verify_dsl_rbi(tmp_dir: dir)
236
+
237
+ report_diff_and_exit_if_out_of_date(diff, "dsl")
238
+ ensure
239
+ FileUtils.remove_entry(dir)
240
+ end
241
+
242
+ sig { params(files: T::Set[Pathname]).void }
243
+ def purge_stale_dsl_rbi_files(files)
244
+ if files.any?
245
+ say("Removing stale RBI files...")
246
+
247
+ files.sort.each do |filename|
248
+ remove(filename)
249
+ end
250
+ say("")
251
+ end
252
+ end
253
+
254
+ sig { params(constant_name: String).returns(Pathname) }
255
+ def dsl_rbi_filename(constant_name)
256
+ @outpath / "#{underscore(constant_name)}.rbi"
257
+ end
258
+
259
+ sig { params(filename: Pathname).void }
260
+ def remove(filename)
261
+ return unless filename.exist?
262
+ say("-- Removing: #{filename}")
263
+ filename.unlink
264
+ end
265
+
266
+ sig { params(tmp_dir: Pathname).returns(T::Hash[String, Symbol]) }
267
+ def verify_dsl_rbi(tmp_dir:)
268
+ diff = {}
269
+
270
+ existing_rbis = rbi_files_in(@outpath)
271
+ new_rbis = rbi_files_in(tmp_dir)
272
+
273
+ added_files = (new_rbis - existing_rbis)
274
+
275
+ added_files.each do |file|
276
+ diff[file] = :added
277
+ end
278
+
279
+ removed_files = (existing_rbis - new_rbis)
280
+
281
+ removed_files.each do |file|
282
+ diff[file] = :removed
283
+ end
284
+
285
+ common_files = (existing_rbis & new_rbis)
286
+
287
+ changed_files = common_files.map do |filename|
288
+ filename unless FileUtils.identical?(@outpath / filename, tmp_dir / filename)
289
+ end.compact
290
+
291
+ changed_files.each do |file|
292
+ diff[file] = :changed
293
+ end
294
+
295
+ diff
296
+ end
297
+
298
+ sig { params(cause: Symbol, files: T::Array[String]).returns(String) }
299
+ def build_error_for_files(cause, files)
300
+ filenames = files.map do |file|
301
+ @outpath / file
302
+ end.join("\n - ")
303
+
304
+ " File(s) #{cause}:\n - #{filenames}"
305
+ end
306
+
307
+ sig { params(diff: T::Hash[String, Symbol], command: String).void }
308
+ def report_diff_and_exit_if_out_of_date(diff, command)
309
+ if diff.empty?
310
+ say("Nothing to do, all RBIs are up-to-date.")
311
+ else
312
+ say("RBI files are out-of-date. In your development environment, please run:", :green)
313
+ say(" `#{@default_command} #{command}`", [:green, :bold])
314
+ say("Once it is complete, be sure to commit and push any changes", :green)
315
+
316
+ say("")
317
+
318
+ say("Reason:", [:red])
319
+ diff.group_by(&:last).sort.each do |cause, diff_for_cause|
320
+ say(build_error_for_files(cause, diff_for_cause.map(&:first)))
321
+ end
322
+
323
+ exit(1)
324
+ end
325
+ end
326
+
327
+ sig { params(path: Pathname).returns(T::Array[Pathname]) }
328
+ def rbi_files_in(path)
329
+ Pathname.glob(path / "**/*.rbi").map do |file|
330
+ file.relative_path_from(path)
331
+ end.sort
332
+ end
333
+
334
+ sig { returns(Loader) }
335
+ def loader
336
+ @loader ||= Loader.new
337
+ end
338
+
339
+ sig { params(class_name: String).returns(String) }
340
+ def underscore(class_name)
341
+ return class_name unless /[A-Z-]|::/.match?(class_name)
342
+
343
+ word = class_name.to_s.gsub("::", "/")
344
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
345
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
346
+ word.tr!("-", "_")
347
+ word.downcase!
348
+ word
349
+ end
350
+
351
+ sig { params(constant: String).returns(String) }
352
+ def rbi_filename_for(constant)
353
+ underscore(constant) + ".rbi"
354
+ end
355
+
356
+ sig { params(constant: String).returns(String) }
357
+ def generate_command_for(constant)
358
+ "#{@default_command} dsl #{constant}"
359
+ end
360
+ end
361
+ end
362
+ end