tapioca 0.5.6 → 0.6.0

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +114 -23
  3. data/lib/tapioca/cli.rb +188 -64
  4. data/lib/tapioca/compilers/dsl/active_record_associations.rb +94 -8
  5. data/lib/tapioca/compilers/dsl/active_record_columns.rb +5 -4
  6. data/lib/tapioca/compilers/dsl/active_record_relations.rb +703 -0
  7. data/lib/tapioca/compilers/dsl/active_record_scope.rb +43 -13
  8. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +2 -4
  9. data/lib/tapioca/compilers/dsl/base.rb +25 -42
  10. data/lib/tapioca/compilers/dsl/extensions/frozen_record.rb +29 -0
  11. data/lib/tapioca/compilers/dsl/frozen_record.rb +37 -0
  12. data/lib/tapioca/compilers/dsl/helper/active_record_constants.rb +27 -0
  13. data/lib/tapioca/compilers/dsl/param_helper.rb +52 -0
  14. data/lib/tapioca/compilers/dsl/rails_generators.rb +120 -0
  15. data/lib/tapioca/compilers/dsl_compiler.rb +32 -6
  16. data/lib/tapioca/compilers/sorbet.rb +2 -0
  17. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +47 -46
  18. data/lib/tapioca/executor.rb +79 -0
  19. data/lib/tapioca/gemfile.rb +23 -0
  20. data/lib/tapioca/generators/base.rb +11 -18
  21. data/lib/tapioca/generators/dsl.rb +33 -38
  22. data/lib/tapioca/generators/gem.rb +50 -29
  23. data/lib/tapioca/generators/init.rb +41 -16
  24. data/lib/tapioca/generators/todo.rb +6 -6
  25. data/lib/tapioca/helpers/cli_helper.rb +26 -0
  26. data/lib/tapioca/helpers/config_helper.rb +84 -0
  27. data/lib/tapioca/helpers/test/content.rb +51 -0
  28. data/lib/tapioca/helpers/test/isolation.rb +125 -0
  29. data/lib/tapioca/helpers/test/template.rb +34 -0
  30. data/lib/tapioca/internal.rb +3 -2
  31. data/lib/tapioca/rbi_ext/model.rb +12 -9
  32. data/lib/tapioca/reflection.rb +13 -0
  33. data/lib/tapioca/trackers/autoload.rb +70 -0
  34. data/lib/tapioca/trackers/constant_definition.rb +42 -0
  35. data/lib/tapioca/trackers/mixin.rb +78 -0
  36. data/lib/tapioca/trackers.rb +14 -0
  37. data/lib/tapioca/version.rb +1 -1
  38. data/lib/tapioca.rb +28 -2
  39. metadata +19 -7
  40. data/lib/tapioca/config.rb +0 -45
  41. data/lib/tapioca/config_builder.rb +0 -73
  42. data/lib/tapioca/constant_locator.rb +0 -40
@@ -7,6 +7,8 @@ rescue LoadError
7
7
  return
8
8
  end
9
9
 
10
+ require "tapioca/compilers/dsl/helper/active_record_constants"
11
+
10
12
  module Tapioca
11
13
  module Compilers
12
14
  module Dsl
@@ -42,6 +44,7 @@ module Tapioca
42
44
  # ~~~
43
45
  class ActiveRecordScope < Base
44
46
  extend T::Sig
47
+ include Helper::ActiveRecordConstants
45
48
 
46
49
  sig do
47
50
  override.params(
@@ -50,19 +53,33 @@ module Tapioca
50
53
  ).void
51
54
  end
52
55
  def decorate(root, constant)
53
- scope_method_names = constant.send(:generated_relation_methods).instance_methods(false)
54
- return if scope_method_names.empty?
56
+ method_names = scope_method_names(constant)
57
+
58
+ return if method_names.empty?
55
59
 
56
60
  root.create_path(constant) do |model|
57
- module_name = "GeneratedRelationMethods"
61
+ relations_enabled = generator_enabled?("ActiveRecordRelations")
62
+
63
+ relation_methods_module = model.create_module(RelationMethodsModuleName)
64
+ assoc_relation_methods_mod = model.create_module(AssociationRelationMethodsModuleName) if relations_enabled
65
+
66
+ method_names.each do |scope_method|
67
+ generate_scope_method(
68
+ relation_methods_module,
69
+ scope_method.to_s,
70
+ relations_enabled ? RelationClassName : "T.untyped"
71
+ )
58
72
 
59
- model.create_module(module_name) do |mod|
60
- scope_method_names.each do |scope_method|
61
- generate_scope_method(scope_method.to_s, mod)
62
- end
73
+ next unless relations_enabled
74
+
75
+ generate_scope_method(
76
+ assoc_relation_methods_mod,
77
+ scope_method.to_s,
78
+ AssociationRelationClassName
79
+ )
63
80
  end
64
81
 
65
- model.create_extend(module_name)
82
+ model.create_extend(RelationMethodsModuleName)
66
83
  end
67
84
  end
68
85
 
@@ -73,16 +90,29 @@ module Tapioca
73
90
 
74
91
  private
75
92
 
93
+ sig { params(constant: T.class_of(::ActiveRecord::Base)).returns(T::Array[Symbol]) }
94
+ def scope_method_names(constant)
95
+ scope_methods = T.let([], T::Array[Symbol])
96
+
97
+ # Keep gathering scope methods until we hit "ActiveRecord::Base"
98
+ until constant == ActiveRecord::Base
99
+ scope_methods.concat(constant.send(:generated_relation_methods).instance_methods(false))
100
+
101
+ # we are guaranteed to have a superclass that is of type "ActiveRecord::Base"
102
+ constant = T.cast(constant.superclass, T.class_of(ActiveRecord::Base))
103
+ end
104
+
105
+ scope_methods
106
+ end
107
+
76
108
  sig do
77
109
  params(
78
- scope_method: String,
79
110
  mod: RBI::Scope,
111
+ scope_method: String,
112
+ return_type: String
80
113
  ).void
81
114
  end
82
- def generate_scope_method(scope_method, mod)
83
- # This return type should actually be Model::ActiveRecord_Relation
84
- return_type = "T.untyped"
85
-
115
+ def generate_scope_method(mod, scope_method, return_type)
86
116
  mod.create_method(
87
117
  scope_method,
88
118
  parameters: [
@@ -109,10 +109,8 @@ module Tapioca
109
109
  type = type_for(field.type_sym)
110
110
  type = "T.nilable(#{type})" if field.null && type != "T.untyped"
111
111
 
112
- model.create_module("StoreAccessors") do |store_accessors_module|
113
- generate_methods(store_accessors_module, field.name.to_s, type)
114
- end
115
-
112
+ store_accessors_module = model.create_module("StoreAccessors")
113
+ generate_methods(store_accessors_module, field.name.to_s, type)
116
114
  model.create_include("StoreAccessors")
117
115
  end
118
116
  end
@@ -2,11 +2,13 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "tapioca/rbi_ext/model"
5
+ require "tapioca/compilers/dsl/param_helper"
6
+ require "tapioca/compilers/dsl_compiler"
5
7
 
6
8
  module Tapioca
7
9
  module Compilers
8
10
  module Dsl
9
- COMPILERS_PATH = T.let(File.expand_path("..", __FILE__).to_s, String)
11
+ DSL_COMPILERS_DIR = T.let(File.expand_path("..", __FILE__).to_s, String)
10
12
 
11
13
  class Base
12
14
  extend T::Sig
@@ -22,8 +24,22 @@ module Tapioca
22
24
  sig { returns(T::Array[String]) }
23
25
  attr_reader :errors
24
26
 
25
- sig { void }
26
- def initialize
27
+ sig { params(name: String).returns(T.nilable(T.class_of(Tapioca::Compilers::Dsl::Base))) }
28
+ def self.resolve(name)
29
+ # Try to find built-in tapioca generator first, then globally defined generator.
30
+ potentials = ["Tapioca::Compilers::Dsl::#{name}", name].map do |potential_name|
31
+ Object.const_get(potential_name)
32
+ rescue NameError
33
+ # Skip if we can't find generator by the potential name
34
+ nil
35
+ end
36
+
37
+ potentials.compact.first
38
+ end
39
+
40
+ sig { params(compiler: Tapioca::Compilers::DslCompiler).void }
41
+ def initialize(compiler)
42
+ @compiler = compiler
27
43
  @processable_constants = T.let(Set.new(gather_constants), T::Set[Module])
28
44
  @processable_constants.compare_by_identity
29
45
  @errors = T.let([], T::Array[String])
@@ -34,6 +50,11 @@ module Tapioca
34
50
  processable_constants.include?(constant)
35
51
  end
36
52
 
53
+ sig { params(generator_name: String).returns(T::Boolean) }
54
+ def generator_enabled?(generator_name)
55
+ @compiler.generator_enabled?(generator_name)
56
+ end
57
+
37
58
  sig do
38
59
  abstract
39
60
  .type_parameters(:T)
@@ -107,45 +128,7 @@ module Tapioca
107
128
  )
108
129
  end
109
130
 
110
- sig { params(name: String, type: String).returns(RBI::TypedParam) }
111
- def create_param(name, type:)
112
- create_typed_param(RBI::Param.new(name), type)
113
- end
114
-
115
- sig { params(name: String, type: String, default: String).returns(RBI::TypedParam) }
116
- def create_opt_param(name, type:, default:)
117
- create_typed_param(RBI::OptParam.new(name, default), type)
118
- end
119
-
120
- sig { params(name: String, type: String).returns(RBI::TypedParam) }
121
- def create_rest_param(name, type:)
122
- create_typed_param(RBI::RestParam.new(name), type)
123
- end
124
-
125
- sig { params(name: String, type: String).returns(RBI::TypedParam) }
126
- def create_kw_param(name, type:)
127
- create_typed_param(RBI::KwParam.new(name), type)
128
- end
129
-
130
- sig { params(name: String, type: String, default: String).returns(RBI::TypedParam) }
131
- def create_kw_opt_param(name, type:, default:)
132
- create_typed_param(RBI::KwOptParam.new(name, default), type)
133
- end
134
-
135
- sig { params(name: String, type: String).returns(RBI::TypedParam) }
136
- def create_kw_rest_param(name, type:)
137
- create_typed_param(RBI::KwRestParam.new(name), type)
138
- end
139
-
140
- sig { params(name: String, type: String).returns(RBI::TypedParam) }
141
- def create_block_param(name, type:)
142
- create_typed_param(RBI::BlockParam.new(name), type)
143
- end
144
-
145
- sig { params(param: RBI::Param, type: String).returns(RBI::TypedParam) }
146
- def create_typed_param(param, type)
147
- RBI::TypedParam.new(param: param, type: type)
148
- end
131
+ include ParamHelper
149
132
 
150
133
  sig { params(method_def: T.any(Method, UnboundMethod)).returns(T::Array[RBI::TypedParam]) }
151
134
  def compile_method_parameters_to_rbi(method_def)
@@ -0,0 +1,29 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "frozen_record"
6
+ rescue LoadError
7
+ return
8
+ end
9
+
10
+ module Tapioca
11
+ module Compilers
12
+ module Dsl
13
+ module Extensions
14
+ module FrozenRecord
15
+ attr_reader :__tapioca_scope_names
16
+
17
+ def scope(name, body)
18
+ @__tapioca_scope_names ||= []
19
+ @__tapioca_scope_names << name
20
+
21
+ super
22
+ end
23
+
24
+ ::FrozenRecord::Base.singleton_class.prepend(self)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -81,6 +81,8 @@ module Tapioca
81
81
  end
82
82
 
83
83
  record.create_include(module_name)
84
+
85
+ decorate_scopes(constant, record)
84
86
  end
85
87
  end
86
88
 
@@ -88,6 +90,41 @@ module Tapioca
88
90
  def gather_constants
89
91
  descendants_of(::FrozenRecord::Base).reject(&:abstract_class?)
90
92
  end
93
+
94
+ private
95
+
96
+ sig { params(constant: T.class_of(::FrozenRecord::Base), record: RBI::Scope).void }
97
+ def decorate_scopes(constant, record)
98
+ scopes = T.unsafe(constant).__tapioca_scope_names
99
+ return if scopes.nil?
100
+
101
+ module_name = "GeneratedRelationMethods"
102
+
103
+ record.create_module(module_name) do |mod|
104
+ scopes.each do |name|
105
+ generate_scope_method(name.to_s, mod)
106
+ end
107
+ end
108
+
109
+ record.create_extend(module_name)
110
+ end
111
+
112
+ sig do
113
+ params(
114
+ scope_method: String,
115
+ mod: RBI::Scope,
116
+ ).void
117
+ end
118
+ def generate_scope_method(scope_method, mod)
119
+ mod.create_method(
120
+ scope_method,
121
+ parameters: [
122
+ create_rest_param("args", type: "T.untyped"),
123
+ create_block_param("blk", type: "T.untyped"),
124
+ ],
125
+ return_type: "T.untyped",
126
+ )
127
+ end
91
128
  end
92
129
  end
93
130
  end
@@ -0,0 +1,27 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Compilers
6
+ module Dsl
7
+ module Helper
8
+ module ActiveRecordConstants
9
+ extend T::Sig
10
+
11
+ AttributeMethodsModuleName = T.let("GeneratedAttributeMethods", String)
12
+ AssociationMethodsModuleName = T.let("GeneratedAssociationMethods", String)
13
+
14
+ RelationMethodsModuleName = T.let("GeneratedRelationMethods", String)
15
+ AssociationRelationMethodsModuleName = T.let("GeneratedAssociationRelationMethods", String)
16
+ CommonRelationMethodsModuleName = T.let("CommonRelationMethods", String)
17
+
18
+ RelationClassName = T.let("PrivateRelation", String)
19
+ RelationWhereChainClassName = T.let("PrivateRelationWhereChain", String)
20
+ AssociationRelationClassName = T.let("PrivateAssociationRelation", String)
21
+ AssociationRelationWhereChainClassName = T.let("PrivateAssociationRelationWhereChain", String)
22
+ AssociationsCollectionProxyClassName = T.let("PrivateCollectionProxy", String)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,52 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Compilers
6
+ module Dsl
7
+ module ParamHelper
8
+ extend T::Sig
9
+
10
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
11
+ def create_param(name, type:)
12
+ create_typed_param(RBI::Param.new(name), type)
13
+ end
14
+
15
+ sig { params(name: String, type: String, default: String).returns(RBI::TypedParam) }
16
+ def create_opt_param(name, type:, default:)
17
+ create_typed_param(RBI::OptParam.new(name, default), type)
18
+ end
19
+
20
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
21
+ def create_rest_param(name, type:)
22
+ create_typed_param(RBI::RestParam.new(name), type)
23
+ end
24
+
25
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
26
+ def create_kw_param(name, type:)
27
+ create_typed_param(RBI::KwParam.new(name), type)
28
+ end
29
+
30
+ sig { params(name: String, type: String, default: String).returns(RBI::TypedParam) }
31
+ def create_kw_opt_param(name, type:, default:)
32
+ create_typed_param(RBI::KwOptParam.new(name, default), type)
33
+ end
34
+
35
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
36
+ def create_kw_rest_param(name, type:)
37
+ create_typed_param(RBI::KwRestParam.new(name), type)
38
+ end
39
+
40
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
41
+ def create_block_param(name, type:)
42
+ create_typed_param(RBI::BlockParam.new(name), type)
43
+ end
44
+
45
+ sig { params(param: RBI::Param, type: String).returns(RBI::TypedParam) }
46
+ def create_typed_param(param, type)
47
+ RBI::TypedParam.new(param: param, type: type)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,120 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "rails/generators"
6
+ require "rails/generators/app_base"
7
+ rescue LoadError
8
+ return
9
+ end
10
+
11
+ module Tapioca
12
+ module Compilers
13
+ module Dsl
14
+ # `Tapioca::Compilers::Dsl::RailsGenerators` generates RBI files for Rails generators
15
+ #
16
+ # For example, with the following generator:
17
+ #
18
+ # ~~~rb
19
+ # # lib/generators/sample_generator.rb
20
+ # class ServiceGenerator < Rails::Generators::NamedBase
21
+ # argument :result_type, type: :string
22
+ #
23
+ # class_option :skip_comments, type: :boolean, default: false
24
+ # end
25
+ # ~~~
26
+ #
27
+ # this compiler will produce the RBI file `service_generator.rbi` with the following content:
28
+ #
29
+ # ~~~rbi
30
+ # # service_generator.rbi
31
+ # # typed: strong
32
+ #
33
+ # class ServiceGenerator
34
+ # sig { returns(::String)}
35
+ # def result_type; end
36
+ #
37
+ # sig { returns(T::Boolean)}
38
+ # def skip_comments; end
39
+ # end
40
+ # ~~~
41
+ class RailsGenerators < Base
42
+ extend T::Sig
43
+
44
+ BUILT_IN_MATCHER = T.let(
45
+ /::(ActionMailbox|ActionText|ActiveRecord|Rails)::Generators/,
46
+ Regexp
47
+ )
48
+
49
+ sig { override.params(root: RBI::Tree, constant: T.class_of(::Rails::Generators::Base)).void }
50
+ def decorate(root, constant)
51
+ base_class = base_class_for(constant)
52
+ arguments = constant.arguments - base_class.arguments
53
+ class_options = constant.class_options.reject do |name, option|
54
+ base_class.class_options[name] == option
55
+ end
56
+
57
+ return if arguments.empty? && class_options.empty?
58
+
59
+ root.create_path(constant) do |klass|
60
+ arguments.each { |argument| generate_methods_for_argument(klass, argument) }
61
+ class_options.each { |_name, option| generate_methods_for_argument(klass, option) }
62
+ end
63
+ end
64
+
65
+ sig { override.returns(T::Enumerable[Module]) }
66
+ def gather_constants
67
+ all_modules.select do |const|
68
+ name = qualified_name_of(const)
69
+
70
+ name &&
71
+ !name.match?(BUILT_IN_MATCHER) &&
72
+ const < ::Rails::Generators::Base
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ sig { params(klass: RBI::Tree, argument: T.any(Thor::Argument, Thor::Option)).void }
79
+ def generate_methods_for_argument(klass, argument)
80
+ klass.create_method(
81
+ argument.name,
82
+ parameters: [],
83
+ return_type: type_for(argument)
84
+ )
85
+ end
86
+
87
+ sig do
88
+ params(constant: T.class_of(::Rails::Generators::Base))
89
+ .returns(T.class_of(::Rails::Generators::Base))
90
+ end
91
+ def base_class_for(constant)
92
+ ancestor = inherited_ancestors_of(constant).find do |klass|
93
+ qualified_name_of(klass)&.match?(BUILT_IN_MATCHER)
94
+ end
95
+
96
+ T.cast(ancestor, T.class_of(::Rails::Generators::Base))
97
+ end
98
+
99
+ sig { params(arg: T.any(Thor::Argument, Thor::Option)).returns(String) }
100
+ def type_for(arg)
101
+ type =
102
+ case arg.type
103
+ when :array then "T::Array[::String]"
104
+ when :boolean then "T::Boolean"
105
+ when :hash then "T::Hash[::String, ::String]"
106
+ when :numeric then "::Numeric"
107
+ when :string then "::String"
108
+ else "T.untyped"
109
+ end
110
+
111
+ if arg.required || arg.default
112
+ type
113
+ else
114
+ "T.nilable(#{type})"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -22,19 +22,31 @@ module Tapioca
22
22
  requested_constants: T::Array[Module],
23
23
  requested_generators: T::Array[T.class_of(Dsl::Base)],
24
24
  excluded_generators: T::Array[T.class_of(Dsl::Base)],
25
- error_handler: T.nilable(T.proc.params(error: String).void)
25
+ error_handler: T.proc.params(error: String).void,
26
+ number_of_workers: T.nilable(Integer),
26
27
  ).void
27
28
  end
28
- def initialize(requested_constants:, requested_generators: [], excluded_generators: [], error_handler: nil)
29
+ def initialize(
30
+ requested_constants:,
31
+ requested_generators: [],
32
+ excluded_generators: [],
33
+ error_handler: $stderr.method(:puts).to_proc,
34
+ number_of_workers: nil
35
+ )
29
36
  @generators = T.let(
30
37
  gather_generators(requested_generators, excluded_generators),
31
38
  T::Enumerable[Dsl::Base]
32
39
  )
33
40
  @requested_constants = requested_constants
34
- @error_handler = T.let(error_handler || $stderr.method(:puts), T.proc.params(error: String).void)
41
+ @error_handler = error_handler
42
+ @number_of_workers = number_of_workers
35
43
  end
36
44
 
37
- sig { params(blk: T.proc.params(constant: Module, rbi: RBI::File).void).void }
45
+ sig do
46
+ type_parameters(:T).params(
47
+ blk: T.proc.params(constant: Module, rbi: RBI::File).returns(T.type_parameter(:T))
48
+ ).returns(T::Array[T.type_parameter(:T)])
49
+ end
38
50
  def run(&blk)
39
51
  constants_to_process = gather_constants(requested_constants)
40
52
  .select { |c| Reflection.name_of(c) && Module === c } # Filter anonymous or value constants
@@ -47,7 +59,10 @@ module Tapioca
47
59
  ERROR
48
60
  end
49
61
 
50
- constants_to_process.each do |constant|
62
+ result = Executor.new(
63
+ constants_to_process,
64
+ number_of_workers: @number_of_workers
65
+ ).run_in_parallel do |constant|
51
66
  rbi = rbi_for_constant(constant)
52
67
  next if rbi.nil?
53
68
 
@@ -57,6 +72,17 @@ module Tapioca
57
72
  generators.flat_map(&:errors).each do |msg|
58
73
  report_error(msg)
59
74
  end
75
+
76
+ result.compact
77
+ end
78
+
79
+ sig { params(generator_name: String).returns(T::Boolean) }
80
+ def generator_enabled?(generator_name)
81
+ generator = Dsl::Base.resolve(generator_name)
82
+
83
+ return false unless generator
84
+
85
+ @generators.any?(generator)
60
86
  end
61
87
 
62
88
  private
@@ -73,7 +99,7 @@ module Tapioca
73
99
  !excluded_generators.include?(klass)
74
100
  end.sort_by { |klass| T.must(klass.name) }
75
101
 
76
- generator_klasses.map(&:new)
102
+ generator_klasses.map { |generator_klass| generator_klass.new(self) }
77
103
  end
78
104
 
79
105
  sig { params(requested_constants: T::Array[Module]).returns(T::Set[Module]) }
@@ -18,6 +18,8 @@ module Tapioca
18
18
  EXE_PATH_ENV_VAR = "TAPIOCA_SORBET_EXE"
19
19
 
20
20
  FEATURE_REQUIREMENTS = T.let({
21
+ # First tag that includes https://github.com/sorbet/sorbet/pull/4706
22
+ to_ary_nil_support: Gem::Requirement.new(">= 0.5.9220"),
21
23
  }.freeze, T::Hash[Symbol, Gem::Requirement])
22
24
 
23
25
  class << self