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
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "parlour"
5
-
6
4
  begin
7
5
  require "rails"
8
6
  require "action_controller"
@@ -89,13 +87,13 @@ module Tapioca
89
87
  class UrlHelpers < Base
90
88
  extend T::Sig
91
89
 
92
- sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: Module).void }
90
+ sig { override.params(root: RBI::Tree, constant: Module).void }
93
91
  def decorate(root, constant)
94
92
  case constant
95
93
  when GeneratedPathHelpersModule.singleton_class, GeneratedUrlHelpersModule.singleton_class
96
94
  generate_module_for(root, constant)
97
95
  else
98
- root.path(constant) do |mod|
96
+ root.create_path(constant) do |mod|
99
97
  create_mixins_for(mod, constant, GeneratedUrlHelpersModule)
100
98
  create_mixins_for(mod, constant, GeneratedPathHelpersModule)
101
99
  end
@@ -112,9 +110,8 @@ module Tapioca
112
110
  Object.const_set(:GeneratedUrlHelpersModule, Rails.application.routes.named_routes.url_helpers_module)
113
111
  Object.const_set(:GeneratedPathHelpersModule, Rails.application.routes.named_routes.path_helpers_module)
114
112
 
115
- module_enumerator = T.cast(ObjectSpace.each_object(Module), T::Enumerator[Module])
116
- constants = module_enumerator.select do |mod|
117
- next unless Module.instance_method(:name).bind(mod).call
113
+ constants = all_modules.select do |mod|
114
+ next unless name_of(mod)
118
115
 
119
116
  includes_helper?(mod, GeneratedUrlHelpersModule) ||
120
117
  includes_helper?(mod, GeneratedPathHelpersModule) ||
@@ -127,7 +124,7 @@ module Tapioca
127
124
 
128
125
  private
129
126
 
130
- sig { params(root: Parlour::RbiGenerator::Namespace, constant: Module).void }
127
+ sig { params(root: RBI::Tree, constant: Module).void }
131
128
  def generate_module_for(root, constant)
132
129
  root.create_module(T.must(constant.name)) do |mod|
133
130
  mod.create_include("::ActionDispatch::Routing::UrlFor")
@@ -136,14 +133,14 @@ module Tapioca
136
133
  constant.instance_methods(false).each do |method|
137
134
  mod.create_method(
138
135
  method.to_s,
139
- parameters: [Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped")],
136
+ parameters: [create_rest_param("args", type: "T.untyped")],
140
137
  return_type: "String"
141
138
  )
142
139
  end
143
140
  end
144
141
  end
145
142
 
146
- sig { params(mod: Parlour::RbiGenerator::Namespace, constant: Module, helper_module: Module).void }
143
+ sig { params(mod: RBI::Scope, constant: Module, helper_module: Module).void }
147
144
  def create_mixins_for(mod, constant, helper_module)
148
145
  include_helper = constant.ancestors.include?(helper_module) || NON_DISCOVERABLE_INCLUDERS.include?(constant)
149
146
  extend_helper = constant.singleton_class.ancestors.include?(helper_module)
@@ -20,20 +20,21 @@ module Tapioca
20
20
  sig do
21
21
  params(
22
22
  requested_constants: T::Array[Module],
23
- requested_generators: T::Array[String],
23
+ requested_generators: T::Array[T.class_of(Dsl::Base)],
24
+ excluded_generators: T::Array[T.class_of(Dsl::Base)],
24
25
  error_handler: T.nilable(T.proc.params(error: String).void)
25
26
  ).void
26
27
  end
27
- def initialize(requested_constants:, requested_generators: [], error_handler: nil)
28
+ def initialize(requested_constants:, requested_generators: [], excluded_generators: [], error_handler: nil)
28
29
  @generators = T.let(
29
- gather_generators(requested_generators),
30
+ gather_generators(requested_generators, excluded_generators),
30
31
  T::Enumerable[Dsl::Base]
31
32
  )
32
33
  @requested_constants = requested_constants
33
34
  @error_handler = T.let(error_handler || $stderr.method(:puts), T.proc.params(error: String).void)
34
35
  end
35
36
 
36
- sig { params(blk: T.proc.params(constant: Module, rbi: String).void).void }
37
+ sig { params(blk: T.proc.params(constant: Module, rbi: RBI::File).void).void }
37
38
  def run(&blk)
38
39
  constants_to_process = gather_constants(requested_constants)
39
40
 
@@ -50,27 +51,27 @@ module Tapioca
50
51
 
51
52
  blk.call(constant, rbi)
52
53
  end
54
+
55
+ generators.flat_map(&:errors).each do |msg|
56
+ report_error(msg)
57
+ end
53
58
  end
54
59
 
55
60
  private
56
61
 
57
- sig { params(requested_generators: T::Array[String]).returns(T.proc.params(klass: Class).returns(T::Boolean)) }
58
- def generator_filter(requested_generators)
59
- return ->(_klass) { 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
62
+ sig do
63
+ params(
64
+ requested_generators: T::Array[T.class_of(Dsl::Base)],
65
+ excluded_generators: T::Array[T.class_of(Dsl::Base)]
66
+ ).returns(T::Enumerable[Dsl::Base])
67
67
  end
68
+ def gather_generators(requested_generators, excluded_generators)
69
+ generator_klasses = ::Tapioca::Reflection.descendants_of(Dsl::Base).select do |klass|
70
+ (requested_generators.empty? || requested_generators.include?(klass)) &&
71
+ !excluded_generators.include?(klass)
72
+ end.sort_by { |klass| T.must(klass.name) }
68
73
 
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
- T.cast(Dsl::Base.descendants.select(&generator_filter).map(&:new), T::Enumerable[Dsl::Base])
74
+ generator_klasses.map(&:new)
74
75
  end
75
76
 
76
77
  sig { params(requested_constants: T::Array[Module]).returns(T::Set[Module]) }
@@ -80,34 +81,18 @@ module Tapioca
80
81
  constants
81
82
  end
82
83
 
83
- sig { params(constant: Module).returns(T.nilable(String)) }
84
+ sig { params(constant: Module).returns(T.nilable(RBI::File)) }
84
85
  def rbi_for_constant(constant)
85
- parlour = Parlour::RbiGenerator.new(sort_namespaces: true)
86
+ file = RBI::File.new(strictness: "true")
86
87
 
87
88
  generators.each do |generator|
88
89
  next unless generator.handles?(constant)
89
- generator.decorate(parlour.root, constant)
90
+ generator.decorate(file.root, constant)
90
91
  end
91
92
 
92
- return if parlour.root.children.empty?
93
-
94
- resolve_conflicts(parlour)
93
+ return if file.root.empty?
95
94
 
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
95
+ file
111
96
  end
112
97
 
113
98
  sig { params(error: String).returns(T.noreturn) }
@@ -0,0 +1,198 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class DynamicMixinCompiler
5
+ extend T::Sig
6
+ include Tapioca::Reflection
7
+
8
+ sig { returns(T::Array[Module]) }
9
+ attr_reader :dynamic_extends, :dynamic_includes
10
+
11
+ sig { returns(T::Array[Symbol]) }
12
+ attr_reader :class_attribute_readers, :class_attribute_writers, :class_attribute_predicates
13
+
14
+ sig { returns(T::Array[Symbol]) }
15
+ attr_reader :instance_attribute_readers, :instance_attribute_writers, :instance_attribute_predicates
16
+
17
+ sig { params(constant: Module).void }
18
+ def initialize(constant)
19
+ @constant = constant
20
+ mixins_from_modules = {}.compare_by_identity
21
+ class_attribute_readers = T.let([], T::Array[Symbol])
22
+ class_attribute_writers = T.let([], T::Array[Symbol])
23
+ class_attribute_predicates = T.let([], T::Array[Symbol])
24
+
25
+ instance_attribute_readers = T.let([], T::Array[Symbol])
26
+ instance_attribute_writers = T.let([], T::Array[Symbol])
27
+ instance_attribute_predicates = T.let([], T::Array[Symbol])
28
+
29
+ Class.new do
30
+ # Override the `self.include` method
31
+ define_singleton_method(:include) do |mod|
32
+ # Take a snapshot of the list of singleton class ancestors
33
+ # before the actual include
34
+ before = singleton_class.ancestors
35
+ # Call the actual `include` method with the supplied module
36
+ super(mod).tap do
37
+ # Take a snapshot of the list of singleton class ancestors
38
+ # after the actual include
39
+ after = singleton_class.ancestors
40
+ # The difference is the modules that are added to the list
41
+ # of ancestors of the singleton class. Those are all the
42
+ # modules that were `extend`ed due to the `include` call.
43
+ #
44
+ # We record those modules on our lookup table keyed by
45
+ # the included module with the values being all the modules
46
+ # that that module pulls into the singleton class.
47
+ #
48
+ # We need to reverse the order, since the extend order should
49
+ # be the inverse of the ancestor order. That is, earlier
50
+ # extended modules would be later in the ancestor chain.
51
+ mixins_from_modules[mod] = (after - before).reverse!
52
+ end
53
+ rescue Exception # rubocop:disable Lint/RescueException
54
+ # this is a best effort, bail if we can't perform this
55
+ end
56
+
57
+ define_singleton_method(:class_attribute) do |*attrs, **kwargs|
58
+ class_attribute_readers.concat(attrs)
59
+ class_attribute_writers.concat(attrs)
60
+
61
+ instance_predicate = kwargs.fetch(:instance_predicate, true)
62
+ instance_accessor = kwargs.fetch(:instance_accessor, true)
63
+ instance_reader = kwargs.fetch(:instance_reader, instance_accessor)
64
+ instance_writer = kwargs.fetch(:instance_writer, instance_accessor)
65
+
66
+ if instance_reader
67
+ instance_attribute_readers.concat(attrs)
68
+ end
69
+
70
+ if instance_writer
71
+ instance_attribute_writers.concat(attrs)
72
+ end
73
+
74
+ if instance_predicate
75
+ class_attribute_predicates.concat(attrs)
76
+
77
+ if instance_reader
78
+ instance_attribute_predicates.concat(attrs)
79
+ end
80
+ end
81
+
82
+ super(*attrs, **kwargs) if defined?(super)
83
+ end
84
+
85
+ # rubocop:disable Style/MissingRespondToMissing
86
+ T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
87
+ def method_missing(symbol, *args)
88
+ # We need this here so that we can handle any random instance
89
+ # method calls on the fake including class that may be done by
90
+ # the included module during the `self.included` hook.
91
+ end
92
+
93
+ class << self
94
+ extend T::Sig
95
+
96
+ T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
97
+ def method_missing(symbol, *args)
98
+ # Similarly, we need this here so that we can handle any
99
+ # random class method calls on the fake including class
100
+ # that may be done by the included module during the
101
+ # `self.included` hook.
102
+ end
103
+ end
104
+ # rubocop:enable Style/MissingRespondToMissing
105
+ end.include(constant)
106
+
107
+ # The value that corresponds to the original included constant
108
+ # is the list of all dynamically extended modules because of that
109
+ # constant. We grab that value by deleting the key for the original
110
+ # constant.
111
+ @dynamic_extends = T.let(mixins_from_modules.delete(constant) || [], T::Array[Module])
112
+
113
+ # Since we deleted the original constant from the list of keys, all
114
+ # the keys that remain are the ones that are dynamically included modules
115
+ # during the include of the original constant.
116
+ @dynamic_includes = T.let(mixins_from_modules.keys, T::Array[Module])
117
+
118
+ @class_attribute_readers = T.let(class_attribute_readers, T::Array[Symbol])
119
+ @class_attribute_writers = T.let(class_attribute_writers, T::Array[Symbol])
120
+ @class_attribute_predicates = T.let(class_attribute_predicates, T::Array[Symbol])
121
+
122
+ @instance_attribute_readers = T.let(instance_attribute_readers, T::Array[Symbol])
123
+ @instance_attribute_writers = T.let(instance_attribute_writers, T::Array[Symbol])
124
+ @instance_attribute_predicates = T.let(instance_attribute_predicates, T::Array[Symbol])
125
+ end
126
+
127
+ sig { returns(T::Boolean) }
128
+ def empty_attributes?
129
+ @class_attribute_readers.empty? && @class_attribute_writers.empty?
130
+ end
131
+
132
+ sig { params(tree: RBI::Tree).void }
133
+ def compile_class_attributes(tree)
134
+ return if empty_attributes?
135
+
136
+ # Create a synthetic module to hold the generated class methods
137
+ tree << RBI::Module.new("GeneratedClassMethods") do |mod|
138
+ class_attribute_readers.each do |attribute|
139
+ mod << RBI::Method.new(attribute.to_s)
140
+ end
141
+
142
+ class_attribute_writers.each do |attribute|
143
+ mod << RBI::Method.new("#{attribute}=") do |method|
144
+ method << RBI::Param.new("value")
145
+ end
146
+ end
147
+
148
+ class_attribute_predicates.each do |attribute|
149
+ mod << RBI::Method.new("#{attribute}?")
150
+ end
151
+ end
152
+
153
+ # Create a synthetic module to hold the generated instance methods
154
+ tree << RBI::Module.new("GeneratedInstanceMethods") do |mod|
155
+ instance_attribute_readers.each do |attribute|
156
+ mod << RBI::Method.new(attribute.to_s)
157
+ end
158
+
159
+ instance_attribute_writers.each do |attribute|
160
+ mod << RBI::Method.new("#{attribute}=") do |method|
161
+ method << RBI::Param.new("value")
162
+ end
163
+ end
164
+
165
+ instance_attribute_predicates.each do |attribute|
166
+ mod << RBI::Method.new("#{attribute}?")
167
+ end
168
+ end
169
+
170
+ # Add a mixes_in_class_methods and include for the generated modules
171
+ tree << RBI::MixesInClassMethods.new("GeneratedClassMethods")
172
+ tree << RBI::Include.new("GeneratedInstanceMethods")
173
+ end
174
+
175
+ sig { params(tree: RBI::Tree).returns([T::Array[Module], T::Array[Module]]) }
176
+ def compile_mixes_in_class_methods(tree)
177
+ includes = dynamic_includes.select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
178
+ includes.each do |mod|
179
+ qname = qualified_name_of(mod)
180
+ tree << RBI::Include.new(T.must(qname))
181
+ end
182
+
183
+ # If we can generate multiple mixes_in_class_methods, then we want to use all dynamic extends that are not the
184
+ # constant itself
185
+ mixed_in_class_methods = dynamic_extends.select { |mod| mod != @constant }
186
+ return [[], []] if mixed_in_class_methods.empty?
187
+
188
+ mixed_in_class_methods.each do |mod|
189
+ qualified_name = qualified_name_of(mod)
190
+ next if qualified_name.nil? || qualified_name.empty?
191
+ tree << RBI::MixesInClassMethods.new(qualified_name)
192
+ end
193
+
194
+ [mixed_in_class_methods, includes]
195
+ rescue
196
+ [[], []] # silence errors
197
+ end
198
+ end
@@ -1,7 +1,7 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'spoom'
4
+ require "spoom"
5
5
 
6
6
  module Tapioca
7
7
  module Compilers
@@ -87,7 +87,7 @@ module Tapioca
87
87
  sig { params(files: T::Enumerable[String], name: String).returns(T::Boolean) }
88
88
  def name_in_project?(files, name)
89
89
  files.any? do |file|
90
- File.basename(file, '.rb') == name
90
+ File.basename(file, ".rb") == name
91
91
  end
92
92
  end
93
93
  end
@@ -1,15 +1,25 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'pathname'
5
- require 'shellwords'
4
+ require "pathname"
5
+ require "shellwords"
6
6
 
7
7
  module Tapioca
8
8
  module Compilers
9
9
  module Sorbet
10
- SORBET = Pathname.new(Gem::Specification.find_by_name("sorbet-static").full_gem_path) / "libexec" / "sorbet"
10
+ SORBET_GEM_SPEC = T.let(
11
+ Gem::Specification.find_by_name("sorbet-static"),
12
+ Gem::Specification
13
+ )
14
+ SORBET = T.let(
15
+ Pathname.new(SORBET_GEM_SPEC.full_gem_path) / "libexec" / "sorbet",
16
+ Pathname
17
+ )
11
18
  EXE_PATH_ENV_VAR = "TAPIOCA_SORBET_EXE"
12
19
 
20
+ FEATURE_REQUIREMENTS = T.let({
21
+ }.freeze, T::Hash[Symbol, Gem::Requirement])
22
+
13
23
  class << self
14
24
  extend(T::Sig)
15
25
 
@@ -20,7 +30,7 @@ module Tapioca
20
30
  sorbet_path,
21
31
  "--quiet",
22
32
  *args,
23
- ].join(' '),
33
+ ].join(" "),
24
34
  err: "/dev/null"
25
35
  ).read
26
36
  end
@@ -31,6 +41,16 @@ module Tapioca
31
41
  sorbet_path = SORBET if sorbet_path.empty?
32
42
  sorbet_path.to_s.shellescape
33
43
  end
44
+
45
+ sig { params(feature: Symbol, version: T.nilable(Gem::Version)).returns(T::Boolean) }
46
+ def supports?(feature, version: nil)
47
+ version = SORBET_GEM_SPEC.version unless version
48
+ requirement = FEATURE_REQUIREMENTS[feature]
49
+
50
+ raise "Invalid Sorbet feature #{feature}" unless requirement
51
+
52
+ requirement.satisfied_by?(version)
53
+ end
34
54
  end
35
55
  end
36
56
  end