tapioca 0.5.6 → 0.6.0

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