tapioca 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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)