tapioca 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +25 -1
  3. data/README.md +12 -0
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +2 -0
  6. data/lib/tapioca/cli.rb +24 -2
  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/symbol_table/symbol_generator.rb +141 -24
  26. data/lib/tapioca/config.rb +11 -6
  27. data/lib/tapioca/config_builder.rb +19 -9
  28. data/lib/tapioca/constant_locator.rb +1 -0
  29. data/lib/tapioca/core_ext/class.rb +23 -0
  30. data/lib/tapioca/generator.rb +187 -21
  31. data/lib/tapioca/loader.rb +20 -9
  32. data/lib/tapioca/sorbet_config_parser.rb +77 -0
  33. data/lib/tapioca/version.rb +1 -1
  34. metadata +29 -51
@@ -0,0 +1,83 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "rails"
8
+ require "action_controller"
9
+ rescue LoadError
10
+ return
11
+ end
12
+
13
+ module Tapioca
14
+ module Compilers
15
+ module Dsl
16
+ class UrlHelpers < Base
17
+ extend T::Sig
18
+
19
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
20
+ def decorate(root, constant)
21
+ case constant
22
+ when GeneratedPathHelpersModule.singleton_class, GeneratedUrlHelpersModule.singleton_class
23
+ generate_module_for(root, constant)
24
+ else
25
+ root.path(constant) do |mod|
26
+ create_mixins_for(mod, constant, GeneratedUrlHelpersModule)
27
+ create_mixins_for(mod, constant, GeneratedPathHelpersModule)
28
+ end
29
+ end
30
+ end
31
+
32
+ sig { override.returns(T::Enumerable[T.untyped]) }
33
+ def gather_constants
34
+ Object.const_set(:GeneratedUrlHelpersModule, Rails.application.routes.named_routes.url_helpers_module)
35
+ Object.const_set(:GeneratedPathHelpersModule, Rails.application.routes.named_routes.path_helpers_module)
36
+
37
+ constants = ObjectSpace.each_object(Module).select do |mod|
38
+ mod = T.cast(mod, T.class_of(Module))
39
+ next unless Module.instance_method(:name).bind(mod).call
40
+
41
+ includes_helper?(mod, GeneratedUrlHelpersModule) ||
42
+ includes_helper?(mod, GeneratedPathHelpersModule) ||
43
+ includes_helper?(mod.singleton_class, GeneratedUrlHelpersModule) ||
44
+ includes_helper?(mod.singleton_class, GeneratedPathHelpersModule)
45
+ end
46
+
47
+ constants << ActionDispatch::IntegrationTest
48
+ end
49
+
50
+ private
51
+
52
+ sig { params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module)).void }
53
+ def generate_module_for(root, constant)
54
+ root.create_module(T.must(constant.name)) do |mod|
55
+ mod.create_include("ActionDispatch::Routing::UrlFor")
56
+ mod.create_include("ActionDispatch::Routing::PolymorphicRoutes")
57
+
58
+ constant.instance_methods(false).each do |method|
59
+ mod.create_method(
60
+ method.to_s,
61
+ parameters: [Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped")],
62
+ return_type: "String"
63
+ )
64
+ end
65
+ end
66
+ end
67
+
68
+ sig { params(mod: Parlour::RbiGenerator::Namespace, constant: T.class_of(Module), helper_module: Module).void }
69
+ def create_mixins_for(mod, constant, helper_module)
70
+ mod.create_include(T.must(helper_module.name)) if constant.ancestors.include?(helper_module)
71
+ mod.create_extend(T.must(helper_module.name)) if constant.singleton_class.ancestors.include?(helper_module)
72
+ end
73
+
74
+ sig { params(mod: Module, helper: Module).returns(T::Boolean) }
75
+ def includes_helper?(mod, helper)
76
+ superclass_ancestors = mod.superclass&.ancestors if Class === mod
77
+ superclass_ancestors ||= []
78
+ (mod.ancestors - superclass_ancestors).include?(helper)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ require "tapioca/compilers/dsl/base"
5
+
6
+ module Tapioca
7
+ module Compilers
8
+ class DslCompiler
9
+ extend T::Sig
10
+
11
+ sig { returns(T::Enumerable[Dsl::Base]) }
12
+ attr_reader :generators
13
+
14
+ sig { returns(T::Array[Module]) }
15
+ attr_reader :requested_constants
16
+
17
+ sig { returns(T.proc.params(error: String).void) }
18
+ attr_reader :error_handler
19
+
20
+ sig do
21
+ params(
22
+ requested_constants: T::Array[Module],
23
+ requested_generators: T::Array[String],
24
+ error_handler: T.nilable(T.proc.params(error: String).void)
25
+ ).void
26
+ end
27
+ def initialize(requested_constants:, requested_generators: [], error_handler: nil)
28
+ @generators = T.let(
29
+ gather_generators(requested_generators),
30
+ T::Enumerable[Dsl::Base]
31
+ )
32
+ @requested_constants = requested_constants
33
+ @error_handler = error_handler || $stderr.method(:puts)
34
+ end
35
+
36
+ sig { params(blk: T.proc.params(constant: Module, rbi: String).void).void }
37
+ def run(&blk)
38
+ constants_to_process = gather_constants(requested_constants)
39
+
40
+ if constants_to_process.empty?
41
+ report_error(<<~ERROR)
42
+ No classes/modules can be matched for RBI generation.
43
+ Please check that the requested classes/modules include processable DSL methods.
44
+ ERROR
45
+ end
46
+
47
+ constants_to_process.sort_by { |c| c.name.to_s }.each do |constant|
48
+ rbi = rbi_for_constant(constant)
49
+ next if rbi.nil?
50
+
51
+ blk.call(constant, rbi)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ sig { params(requested_generators: T::Array[String]).returns(Proc) }
58
+ def generator_filter(requested_generators)
59
+ return proc { true } if requested_generators.empty?
60
+
61
+ generators = requested_generators.map(&:downcase)
62
+
63
+ proc do |klass|
64
+ generator = klass.name&.sub(/^Tapioca::Compilers::Dsl::/, '')&.downcase
65
+ generators.include?(generator)
66
+ end
67
+ end
68
+
69
+ sig { params(requested_generators: T::Array[String]).returns(T::Enumerable[Dsl::Base]) }
70
+ def gather_generators(requested_generators)
71
+ generator_filter = generator_filter(requested_generators)
72
+
73
+ Dsl::Base.descendants.select(&generator_filter).map(&:new)
74
+ end
75
+
76
+ sig { params(requested_constants: T::Array[Module]).returns(T::Set[Module]) }
77
+ def gather_constants(requested_constants)
78
+ constants = generators.map(&:processable_constants).reduce(Set.new, :union)
79
+ constants &= requested_constants unless requested_constants.empty?
80
+ constants
81
+ end
82
+
83
+ sig { params(constant: Module).returns(T.nilable(String)) }
84
+ def rbi_for_constant(constant)
85
+ parlour = Parlour::RbiGenerator.new(sort_namespaces: true)
86
+
87
+ generators.each do |generator|
88
+ next unless generator.handles?(constant)
89
+ generator.decorate(parlour.root, constant)
90
+ end
91
+
92
+ return if parlour.root.children.empty?
93
+
94
+ resolve_conflicts(parlour)
95
+
96
+ parlour.rbi("true").strip
97
+ end
98
+
99
+ sig { params(parlour: Parlour::RbiGenerator).void }
100
+ def resolve_conflicts(parlour)
101
+ Parlour::ConflictResolver.new.resolve_conflicts(parlour.root) do |msg, candidates|
102
+ error = StringIO.new
103
+ error.puts "=== Error ==="
104
+ error.puts msg
105
+ error.puts "# Candidates"
106
+ candidates.each_with_index do |candidate, index|
107
+ error.puts " #{index}. #{candidate.describe}"
108
+ end
109
+ report_error(error.string)
110
+ end
111
+ end
112
+
113
+ sig { params(error: String).returns(T.noreturn) }
114
+ def report_error(error)
115
+ handler = error_handler
116
+ handler.call(error)
117
+ exit(1)
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require_relative '../sorbet_config_parser'
5
+
6
+ module Tapioca
7
+ module Compilers
8
+ class RequiresCompiler
9
+ extend T::Sig
10
+
11
+ sig { params(sorbet_path: String).void }
12
+ def initialize(sorbet_path)
13
+ @sorbet_path = sorbet_path
14
+ end
15
+
16
+ sig { returns(String) }
17
+ def compile
18
+ config = SorbetConfig.parse_file(@sorbet_path)
19
+ files = collect_files(config)
20
+ files.flat_map do |file|
21
+ collect_requires(file).reject do |req|
22
+ name_in_project?(files, req)
23
+ end
24
+ end.sort.uniq.map do |name|
25
+ "require '#{name}'\n"
26
+ end.join
27
+ end
28
+
29
+ private
30
+
31
+ sig { params(config: SorbetConfig).returns(T::Array[String]) }
32
+ def collect_files(config)
33
+ config.paths.flat_map do |path|
34
+ path = (Pathname.new(@sorbet_path) / "../.." / path).cleanpath
35
+ if path.directory?
36
+ Dir.glob("#{path}/**/*.rb", File::FNM_EXTGLOB).reject do |file|
37
+ file_ignored_by_sorbet?(config, file)
38
+ end
39
+ else
40
+ [path.to_s]
41
+ end
42
+ end.sort.uniq
43
+ end
44
+
45
+ sig { params(file_path: String).returns(T::Enumerable[String]) }
46
+ def collect_requires(file_path)
47
+ File.read(file_path).lines.map do |line|
48
+ /^\s*require\s*(\(\s*)?['"](?<name>[^'"]+)['"](\s*\))?/.match(line) { |m| m["name"] }
49
+ end.compact
50
+ end
51
+
52
+ sig { params(config: SorbetConfig, file: String).returns(T::Boolean) }
53
+ def file_ignored_by_sorbet?(config, file)
54
+ config.ignore.any? do |path|
55
+ Regexp.new(Regexp.escape(path)) =~ file
56
+ end
57
+ end
58
+
59
+ sig { params(files: T::Enumerable[String], name: String).returns(T::Boolean) }
60
+ def name_in_project?(files, name)
61
+ files.any? do |file|
62
+ File.basename(file, '.rb') == name
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -176,13 +176,43 @@ module Tapioca
176
176
  return if symbol_ignored?(name) && methods.nil?
177
177
 
178
178
  [
179
+ compile_module_helpers(constant),
179
180
  compile_mixins(constant),
180
181
  compile_mixes_in_class_methods(constant),
182
+ compile_props(constant),
181
183
  methods,
182
184
  ].select { |b| b != "" }.join("\n\n")
183
185
  end
184
186
  end
185
187
 
188
+ sig { params(constant: Module).returns(String) }
189
+ def compile_module_helpers(constant)
190
+ abstract_type = T::Private::Abstract::Data.get(constant, :abstract_type)
191
+
192
+ if abstract_type
193
+ indented("#{abstract_type}!")
194
+ elsif T::Private::Final.final_module?(constant)
195
+ indented("final!")
196
+ elsif T::Private::Sealed.sealed_module?(constant)
197
+ indented("sealed!")
198
+ else
199
+ ""
200
+ end
201
+ end
202
+
203
+ sig { params(constant: Module).returns(String) }
204
+ def compile_props(constant)
205
+ return "" unless T::Props::ClassMethods === constant
206
+
207
+ constant.props.map do |name, prop|
208
+ method = "prop"
209
+ method = "const" if prop.fetch(:immutable, false)
210
+ type = prop.fetch(:type_object, "T.untyped")
211
+
212
+ indented("#{method} :#{name}, #{type}")
213
+ end.join("\n")
214
+ end
215
+
186
216
  sig { params(name: String, constant: Module).returns(T.nilable(String)) }
187
217
  def compile_subconstants(name, constant)
188
218
  output = constants_of(constant).sort.uniq.map do |constant_name|
@@ -379,7 +409,7 @@ module Tapioca
379
409
  )
380
410
 
381
411
  instance_methods = compile_directly_owned_methods(name, constant)
382
- singleton_methods = compile_directly_owned_methods(name, singleton_class_of(constant), [:public])
412
+ singleton_methods = compile_directly_owned_methods(name, singleton_class_of(constant))
383
413
 
384
414
  return if symbol_ignored?(name) && instance_methods.empty? && singleton_methods.empty?
385
415
 
@@ -392,24 +422,44 @@ module Tapioca
392
422
 
393
423
  sig { params(module_name: String, mod: Module, for_visibility: T::Array[Symbol]).returns(String) }
394
424
  def compile_directly_owned_methods(module_name, mod, for_visibility = [:public, :protected, :private])
395
- method_names_by_visibility(mod)
396
- .delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
397
- .flat_map do |visibility, method_list|
398
- compiled = method_list.sort!.map do |name|
399
- next if name == :initialize
400
- compile_method(module_name, mod, mod.instance_method(name))
401
- end
402
- compiled.compact!
425
+ indent_step = 0
426
+ preamble = nil
427
+ postamble = nil
428
+
429
+ if mod.singleton_class?
430
+ indent_step = 1
431
+ preamble = indented("class << self")
432
+ postamble = indented("end")
433
+ end
403
434
 
404
- unless compiled.empty? || visibility == :public
405
- # add visibility badge
406
- compiled.unshift('', indented(visibility.to_s), '')
435
+ methods = with_indentation(indent_step) do
436
+ method_names_by_visibility(mod)
437
+ .delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
438
+ .flat_map do |visibility, method_list|
439
+ compiled = method_list.sort!.map do |name|
440
+ next if name == :initialize
441
+ compile_method(module_name, mod, mod.instance_method(name))
442
+ end
443
+ compiled.compact!
444
+
445
+ unless compiled.empty? || visibility == :public
446
+ # add visibility badge
447
+ compiled.unshift('', indented(visibility.to_s), '')
448
+ end
449
+
450
+ compiled
407
451
  end
452
+ .compact
453
+ .join("\n")
454
+ end
408
455
 
409
- compiled
410
- end
411
- .compact
412
- .join("\n")
456
+ return "" if methods.strip == ""
457
+
458
+ [
459
+ preamble,
460
+ methods,
461
+ postamble,
462
+ ].compact.join("\n")
413
463
  end
414
464
 
415
465
  sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
@@ -421,6 +471,16 @@ module Tapioca
421
471
  }
422
472
  end
423
473
 
474
+ sig { params(constant: Module, method_name: String).returns(T::Boolean) }
475
+ def struct_method?(constant, method_name)
476
+ return false unless T::Props::ClassMethods === constant
477
+
478
+ constant
479
+ .props
480
+ .keys
481
+ .include?(method_name.gsub(/=$/, '').to_sym)
482
+ end
483
+
424
484
  sig do
425
485
  params(
426
486
  symbol_name: String,
@@ -433,8 +493,13 @@ module Tapioca
433
493
  return unless method.owner == constant
434
494
  return if symbol_ignored?(symbol_name) && !method_in_gem?(method)
435
495
 
496
+ signature = signature_of(method)
497
+ method = signature.method if signature
498
+
436
499
  method_name = method.name.to_s
437
500
  return unless valid_method_name?(method_name)
501
+ return if struct_method?(constant, method_name)
502
+ return if method_name.start_with?("__t_props_generated_")
438
503
 
439
504
  params = T.let(method.parameters, T::Array[T::Array[Symbol]])
440
505
  parameters = params.map do |(type, name)|
@@ -447,13 +512,13 @@ module Tapioca
447
512
  when :req
448
513
  name
449
514
  when :opt
450
- "#{name} = _"
515
+ "#{name} = T.unsafe(nil)"
451
516
  when :rest
452
517
  "*#{name}"
453
518
  when :keyreq
454
519
  "#{name}:"
455
520
  when :key
456
- "#{name}: _"
521
+ "#{name}: T.unsafe(nil)"
457
522
  when :keyrest
458
523
  "**#{name}"
459
524
  when :block
@@ -461,10 +526,54 @@ module Tapioca
461
526
  end
462
527
  end.join(', ')
463
528
 
464
- method_name = "#{'self.' if constant.singleton_class?}#{method_name}"
465
529
  parameters = "(#{parameters})" if parameters != ""
466
530
 
467
- indented("def #{method_name}#{parameters}; end")
531
+ signature_str = indented(compile_signature(signature)) if signature
532
+ [
533
+ signature_str,
534
+ indented("def #{method_name}#{parameters}; end"),
535
+ ].compact.join("\n")
536
+ end
537
+
538
+ TYPE_PARAMETER_MATCHER = /T\.type_parameter\(:?([[:word:]]+)\)/
539
+
540
+ sig { params(signature: T.untyped).returns(String) }
541
+ def compile_signature(signature)
542
+ params = signature.arg_types
543
+ params += signature.kwarg_types.to_a
544
+ params << [signature.rest_name, signature.rest_type] if signature.has_rest
545
+ params << [signature.block_name, signature.block_type] if signature.block_name
546
+
547
+ params = params.compact.map { |name, type| "#{name}: #{type}" }.join(", ")
548
+ returns = signature.return_type.to_s
549
+
550
+ type_parameters = (params + returns).scan(TYPE_PARAMETER_MATCHER).flatten.uniq.map { |p| ":#{p}" }.join(", ")
551
+ type_parameters = ".type_parameters(#{type_parameters})" unless type_parameters.empty?
552
+
553
+ mode = case signature.mode
554
+ when "abstract"
555
+ ".abstract"
556
+ when "override"
557
+ ".override"
558
+ when "overridable_override"
559
+ ".overridable.override"
560
+ when "overridable"
561
+ ".overridable"
562
+ else
563
+ ""
564
+ end
565
+
566
+ signature_body = +""
567
+ signature_body << mode
568
+ signature_body << type_parameters
569
+ signature_body << ".params(#{params})" unless params.empty?
570
+ signature_body << ".returns(#{returns})"
571
+ signature_body = signature_body
572
+ .gsub(".returns(<VOID>)", ".void")
573
+ .gsub("<NOT-TYPED>", "T.untyped")
574
+ .gsub(TYPE_PARAMETER_MATCHER, "T.type_parameter(:\\1)")[1..-1]
575
+
576
+ "sig { #{signature_body} }"
468
577
  end
469
578
 
470
579
  sig { params(symbol_name: String).returns(T::Boolean) }
@@ -483,16 +592,17 @@ module Tapioca
483
592
  sig do
484
593
  type_parameters(:U)
485
594
  .params(
595
+ step: Integer,
486
596
  _blk: T.proc
487
597
  .returns(T.type_parameter(:U))
488
598
  )
489
599
  .returns(T.type_parameter(:U))
490
600
  end
491
- def with_indentation(&_blk)
492
- @indent += 2
601
+ def with_indentation(step = 1, &_blk)
602
+ @indent += 2 * step
493
603
  yield
494
604
  ensure
495
- @indent -= 2
605
+ @indent -= 2 * step
496
606
  end
497
607
 
498
608
  sig { params(str: String).returns(String) }
@@ -627,7 +737,7 @@ module Tapioca
627
737
  return nil
628
738
  end
629
739
 
630
- name_of(target)
740
+ raw_name_of(target)
631
741
  end
632
742
 
633
743
  sig { params(constant: Module).returns(T.nilable(String)) }
@@ -647,6 +757,13 @@ module Tapioca
647
757
  Class.instance_method(:superclass).bind(constant).call
648
758
  end
649
759
 
760
+ sig { params(method: T.any(UnboundMethod, Method)).returns(T.untyped) }
761
+ def signature_of(method)
762
+ T::Private::Methods.signature_for_method(method)
763
+ rescue LoadError, StandardError
764
+ nil
765
+ end
766
+
650
767
  sig { params(constant: Module, other: BasicObject).returns(T::Boolean).checked(:never) }
651
768
  def are_equal?(constant, other)
652
769
  BasicObject.instance_method(:equal?).bind(constant).call(other)