tapioca 0.10.4 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/tapioca/cli.rb +14 -5
  3. data/lib/tapioca/commands/annotations.rb +2 -0
  4. data/lib/tapioca/commands/configure.rb +1 -0
  5. data/lib/tapioca/commands/dsl.rb +17 -3
  6. data/lib/tapioca/commands/gem.rb +4 -2
  7. data/lib/tapioca/dsl/compilers/aasm.rb +78 -17
  8. data/lib/tapioca/dsl/compilers/action_controller_helpers.rb +1 -1
  9. data/lib/tapioca/dsl/compilers/active_record_columns.rb +3 -3
  10. data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +8 -5
  11. data/lib/tapioca/dsl/compilers/active_record_relations.rb +140 -83
  12. data/lib/tapioca/dsl/compilers/active_record_scope.rb +1 -1
  13. data/lib/tapioca/dsl/compilers/active_record_secure_token.rb +74 -0
  14. data/lib/tapioca/dsl/compilers/active_record_typed_store.rb +14 -11
  15. data/lib/tapioca/dsl/compilers/active_resource.rb +22 -15
  16. data/lib/tapioca/dsl/compilers/active_storage.rb +4 -2
  17. data/lib/tapioca/dsl/compilers/graphql_input_object.rb +21 -1
  18. data/lib/tapioca/dsl/compilers/kredis.rb +130 -0
  19. data/lib/tapioca/dsl/compilers/smart_properties.rb +7 -4
  20. data/lib/tapioca/dsl/compilers/url_helpers.rb +7 -4
  21. data/lib/tapioca/dsl/extensions/active_record.rb +9 -0
  22. data/lib/tapioca/dsl/extensions/kredis.rb +114 -0
  23. data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +37 -27
  24. data/lib/tapioca/dsl/helpers/active_record_constants_helper.rb +1 -0
  25. data/lib/tapioca/dsl/pipeline.rb +12 -5
  26. data/lib/tapioca/gem/listeners/sorbet_enums.rb +1 -1
  27. data/lib/tapioca/gem/listeners/yard_doc.rb +13 -10
  28. data/lib/tapioca/gem/pipeline.rb +14 -0
  29. data/lib/tapioca/gemfile.rb +6 -2
  30. data/lib/tapioca/helpers/rbi_files_helper.rb +12 -6
  31. data/lib/tapioca/helpers/sorbet_helper.rb +7 -4
  32. data/lib/tapioca/helpers/source_uri.rb +10 -7
  33. data/lib/tapioca/loaders/gem.rb +4 -2
  34. data/lib/tapioca/loaders/loader.rb +99 -35
  35. data/lib/tapioca/rbi_ext/model.rb +8 -3
  36. data/lib/tapioca/rbi_formatter.rb +11 -8
  37. data/lib/tapioca/runtime/attached_class_of_32.rb +20 -0
  38. data/lib/tapioca/runtime/attached_class_of_legacy.rb +27 -0
  39. data/lib/tapioca/runtime/reflection.rb +11 -10
  40. data/lib/tapioca/runtime/trackers.rb +17 -0
  41. data/lib/tapioca/static/symbol_loader.rb +14 -14
  42. data/lib/tapioca/version.rb +1 -1
  43. data/lib/tapioca.rb +8 -5
  44. metadata +7 -2
@@ -47,44 +47,113 @@ module Tapioca
47
47
 
48
48
  eager_load_rails_app if eager_load
49
49
  rescue LoadError, StandardError => e
50
- say("Tapioca attempted to load the Rails application after encountering a `config/application.rb` file, " \
51
- "but it failed. If your application uses Rails please ensure it can be loaded correctly before generating " \
52
- "RBIs.\n#{e}", :yellow)
50
+ say(
51
+ "Tapioca attempted to load the Rails application after encountering a `config/application.rb` file, " \
52
+ "but it failed. If your application uses Rails please ensure it can be loaded correctly before " \
53
+ "generating RBIs.\n#{e}",
54
+ :yellow,
55
+ )
53
56
  say("Continuing RBI generation without loading the Rails application.")
54
57
  end
55
58
 
56
59
  sig { void }
57
60
  def load_rails_engines
58
- rails_engines.each do |engine|
59
- errored_files = []
61
+ return if engines.empty?
60
62
 
63
+ with_rails_application do
64
+ run_initializers
65
+
66
+ if zeitwerk_mode?
67
+ load_engines_in_zeitwerk_mode
68
+ else
69
+ load_engines_in_classic_mode
70
+ end
71
+ end
72
+ end
73
+
74
+ def run_initializers
75
+ engines.each do |engine|
76
+ engine.instance.initializers.tsort_each do |initializer|
77
+ initializer.run(Rails.application)
78
+ rescue ScriptError, StandardError
79
+ nil
80
+ end
81
+ end
82
+ end
83
+
84
+ sig { void }
85
+ def load_engines_in_zeitwerk_mode
86
+ # Collect all the directories that are already managed by all existing Zeitwerk loaders.
87
+ managed_dirs = Zeitwerk::Registry.loaders.flat_map(&:dirs).to_set
88
+ # We use a fresh loader to load the engine directories, so that we don't interfere with
89
+ # any of the existing loaders.
90
+ autoloader = Zeitwerk::Loader.new
91
+
92
+ engines.each do |engine|
93
+ engine.config.eager_load_paths.each do |path|
94
+ # Zeitwerk only accepts existing directories in `push_dir`.
95
+ next unless File.directory?(path)
96
+ # We should not add directories that are already managed by a Zeitwerk loader.
97
+ next if managed_dirs.member?(path)
98
+
99
+ autoloader.push_dir(path)
100
+ end
101
+ end
102
+
103
+ autoloader.setup
104
+ end
105
+
106
+ sig { void }
107
+ def load_engines_in_classic_mode
108
+ # This is code adapted from `Rails::Engine#eager_load!` in
109
+ # https://github.com/rails/rails/blob/d9e188dbab81b412f73dfb7763318d52f360af49/railties/lib/rails/engine.rb#L489-L495
110
+ #
111
+ # We can't use `Rails::Engine#eager_load!` directly because it will raise as soon as it encounters
112
+ # an error, which is not what we want. We want to try to load as much as we can.
113
+ engines.each do |engine|
61
114
  engine.config.eager_load_paths.each do |load_path|
62
115
  Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
63
- require(file)
64
- rescue LoadError, StandardError
65
- errored_files << file
116
+ require_dependency file
66
117
  end
67
- end
68
-
69
- # Try files that have errored one more time
70
- # It might have been a load order problem
71
- errored_files.each do |file|
72
- require(file)
73
- rescue LoadError, StandardError
118
+ rescue ScriptError, StandardError
74
119
  nil
75
120
  end
76
121
  end
77
122
  end
78
123
 
79
- sig { returns(T::Array[T.untyped]) }
80
- def rails_engines
81
- return [] unless Object.const_defined?("Rails::Engine")
124
+ sig { returns(T::Boolean) }
125
+ def zeitwerk_mode?
126
+ Rails.respond_to?(:autoloaders) &&
127
+ Rails.autoloaders.respond_to?(:zeitwerk_enabled?) &&
128
+ Rails.autoloaders.zeitwerk_enabled?
129
+ end
130
+
131
+ sig { params(blk: T.proc.void).void }
132
+ def with_rails_application(&blk)
133
+ # Store the current Rails.application object so that we can restore it
134
+ rails_application = T.unsafe(Rails.application)
135
+
136
+ # Create a new Rails::Application object, so that we can load the engines.
137
+ # Some engines and the `Rails.autoloaders` call might expect `Rails.application`
138
+ # to be set, so we need to create one here.
139
+ unless rails_application
140
+ Rails.application = Class.new(Rails::Application)
141
+ end
142
+
143
+ blk.call
144
+ ensure
145
+ Rails.app_class = Rails.application = rails_application
146
+ end
147
+
148
+ T::Sig::WithoutRuntime.sig { returns(T::Array[T.class_of(Rails::Engine)]) }
149
+ def engines
150
+ return [] unless defined?(Rails::Engine)
82
151
 
83
152
  safe_require("active_support/core_ext/class/subclasses")
84
153
 
85
154
  project_path = Bundler.default_gemfile.parent.expand_path
86
155
  # We can use `Class#descendants` here, since we know Rails is loaded
87
- Object.const_get("Rails::Engine")
156
+ Rails::Engine
88
157
  .descendants
89
158
  .reject(&:abstract_railtie?)
90
159
  .reject { |engine| gem_in_app_dir?(project_path, engine.config.root.to_path) }
@@ -100,30 +169,25 @@ module Tapioca
100
169
  sig { void }
101
170
  def silence_deprecations
102
171
  # Stop any ActiveSupport Deprecations from being reported
103
- Object.const_get("ActiveSupport::Deprecation").silenced = true
104
- rescue NameError
105
- nil
172
+ if defined?(ActiveSupport::Deprecation)
173
+ ActiveSupport::Deprecation.silenced = true
174
+ end
106
175
  end
107
176
 
108
177
  sig { void }
109
178
  def eager_load_rails_app
110
- rails = Object.const_get("Rails")
111
- application = rails.application
112
-
113
- if Object.const_defined?("ActiveSupport")
114
- Object.const_get("ActiveSupport").run_load_hooks(
115
- :before_eager_load,
116
- application,
117
- )
179
+ application = Rails.application
180
+
181
+ if defined?(ActiveSupport)
182
+ ActiveSupport.run_load_hooks(:before_eager_load, application)
118
183
  end
119
184
 
120
- if Object.const_defined?("Zeitwerk::Loader")
121
- zeitwerk_loader = Object.const_get("Zeitwerk::Loader")
122
- zeitwerk_loader.eager_load_all
185
+ if defined?(Zeitwerk::Loader)
186
+ Zeitwerk::Loader.eager_load_all
123
187
  end
124
188
 
125
- if rails.respond_to?(:autoloaders) && rails.autoloaders.zeitwerk_enabled?
126
- rails.autoloaders.each(&:eager_load)
189
+ if Rails.respond_to?(:autoloaders)
190
+ Rails.autoloaders.each(&:eager_load)
127
191
  end
128
192
 
129
193
  if application.config.respond_to?(:eager_load_namespaces)
@@ -5,7 +5,7 @@ module RBI
5
5
  class Tree
6
6
  extend T::Sig
7
7
 
8
- sig { params(constant: ::Module, block: T.nilable(T.proc.params(scope: Scope).void)).void }
8
+ sig { params(constant: ::Module, block: T.nilable(T.proc.params(scope: Scope).void)).returns(Scope) }
9
9
  def create_path(constant, &block)
10
10
  constant_name = Tapioca::Runtime::Reflection.name_of(constant)
11
11
  raise "given constant does not have a name" unless constant_name
@@ -91,8 +91,13 @@ module RBI
91
91
  return unless Tapioca::RBIHelper.valid_method_name?(name)
92
92
 
93
93
  sig = RBI::Sig.new(return_type: return_type)
94
- method = RBI::Method.new(name, sigs: [sig], is_singleton: class_method, visibility: visibility,
95
- comments: comments)
94
+ method = RBI::Method.new(
95
+ name,
96
+ sigs: [sig],
97
+ is_singleton: class_method,
98
+ visibility: visibility,
99
+ comments: comments,
100
+ )
96
101
  parameters.each do |param|
97
102
  method << param.param
98
103
  sig << RBI::SigParam.new(param.param.name, param.type)
@@ -26,12 +26,15 @@ module Tapioca
26
26
  end
27
27
  end
28
28
 
29
- DEFAULT_RBI_FORMATTER = T.let(RBIFormatter.new(
30
- add_sig_templates: false,
31
- group_nodes: true,
32
- max_line_length: nil,
33
- nest_singleton_methods: true,
34
- nest_non_public_methods: true,
35
- sort_nodes: true,
36
- ), RBIFormatter)
29
+ DEFAULT_RBI_FORMATTER = T.let(
30
+ RBIFormatter.new(
31
+ add_sig_templates: false,
32
+ group_nodes: true,
33
+ max_line_length: nil,
34
+ nest_singleton_methods: true,
35
+ nest_non_public_methods: true,
36
+ sort_nodes: true,
37
+ ),
38
+ RBIFormatter,
39
+ )
37
40
  end
@@ -0,0 +1,20 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Runtime
6
+ # This module should only be included when running Ruby version 3.2
7
+ # or newer. It relies on the Class#attached_object method, which was
8
+ # added in Ruby 3.2 and fetches the attached object of a singleton
9
+ # class without having to iterate through all of ObjectSpace.
10
+ module AttachedClassOf
11
+ extend T::Sig
12
+
13
+ sig { params(singleton_class: Class).returns(T.nilable(Module)) }
14
+ def attached_class_of(singleton_class)
15
+ result = singleton_class.attached_object
16
+ Module === result ? result : nil
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Runtime
6
+ # This module should only be included when running versions of Ruby
7
+ # older than 3.2. Because the Class#attached_object method is not
8
+ # available, it implements finding the attached class of a singleton
9
+ # class by iterating through ObjectSpace.
10
+ module AttachedClassOf
11
+ extend T::Sig
12
+ extend T::Helpers
13
+
14
+ requires_ancestor { Tapioca::Runtime::Reflection }
15
+
16
+ sig { params(singleton_class: Class).returns(T.nilable(Module)) }
17
+ def attached_class_of(singleton_class)
18
+ # https://stackoverflow.com/a/36622320/98634
19
+ result = ObjectSpace.each_object(singleton_class).find do |klass|
20
+ singleton_class_of(T.cast(klass, Module)) == singleton_class
21
+ end
22
+
23
+ T.cast(result, T.nilable(Module))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,9 +1,20 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ # On Ruby 3.2 or newer, Class defines an attached_object method that returns the
5
+ # attached class of a singleton class without iterating ObjectSpace. On older
6
+ # versions of Ruby, we fall back to iterating ObjectSpace.
7
+ if Class.method_defined?(:attached_object)
8
+ require "tapioca/runtime/attached_class_of_32"
9
+ else
10
+ require "tapioca/runtime/attached_class_of_legacy"
11
+ end
12
+
4
13
  module Tapioca
5
14
  module Runtime
6
15
  module Reflection
16
+ include AttachedClassOf
17
+
7
18
  extend T::Sig
8
19
  extend self
9
20
 
@@ -174,16 +185,6 @@ module Tapioca
174
185
  resolved_loc.absolute_path || ""
175
186
  end
176
187
 
177
- sig { params(singleton_class: Module).returns(T.nilable(Module)) }
178
- def attached_class_of(singleton_class)
179
- # https://stackoverflow.com/a/36622320/98634
180
- result = ObjectSpace.each_object(singleton_class).find do |klass|
181
- singleton_class_of(T.cast(klass, Module)) == singleton_class
182
- end
183
-
184
- T.cast(result, Module)
185
- end
186
-
187
188
  sig { params(constant: Module).returns(T::Set[String]) }
188
189
  def file_candidates_for(constant)
189
190
  relevant_methods_for(constant).filter_map do |method|
@@ -13,6 +13,23 @@ module Tapioca
13
13
  class << self
14
14
  extend T::Sig
15
15
 
16
+ sig do
17
+ type_parameters(:Return)
18
+ .params(blk: T.proc.returns(T.type_parameter(:Return)))
19
+ .returns(T.type_parameter(:Return))
20
+ end
21
+ def with_trackers_enabled(&blk)
22
+ # Currently this is a dirty hack to ensure disabling trackers
23
+ # doesn't work while in the block passed to this method.
24
+ disable_all_method = method(:disable_all!)
25
+ define_singleton_method(:disable_all!) {}
26
+ blk.call
27
+ ensure
28
+ if disable_all_method
29
+ define_singleton_method(:disable_all!, disable_all_method)
30
+ end
31
+ end
32
+
16
33
  sig { void }
17
34
  def disable_all!
18
35
  @trackers.each(&:disable!)
@@ -41,6 +41,20 @@ module Tapioca
41
41
  symbols_from_paths(gem.files)
42
42
  end
43
43
 
44
+ sig { params(paths: T::Array[Pathname]).returns(T::Set[String]) }
45
+ def symbols_from_paths(paths)
46
+ output = Tempfile.create("sorbet") do |file|
47
+ file.write(Array(paths).join("\n"))
48
+ file.flush
49
+
50
+ symbol_table_json_from("@#{file.path.shellescape}")
51
+ end
52
+
53
+ return Set.new if output.empty?
54
+
55
+ SymbolTableParser.parse_json(output)
56
+ end
57
+
44
58
  private
45
59
 
46
60
  sig { returns(T::Array[T.class_of(Rails::Engine)]) }
@@ -59,20 +73,6 @@ module Tapioca
59
73
  def symbol_table_json_from(input, table_type: "symbol-table-json")
60
74
  sorbet("--no-config", "--quiet", "--print=#{table_type}", input).out
61
75
  end
62
-
63
- sig { params(paths: T::Array[Pathname]).returns(T::Set[String]) }
64
- def symbols_from_paths(paths)
65
- output = Tempfile.create("sorbet") do |file|
66
- file.write(Array(paths).join("\n"))
67
- file.flush
68
-
69
- symbol_table_json_from("@#{file.path.shellescape}")
70
- end
71
-
72
- return Set.new if output.empty?
73
-
74
- SymbolTableParser.parse_json(output)
75
- end
76
76
  end
77
77
  end
78
78
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
- VERSION = "0.10.4"
5
+ VERSION = "0.11.0"
6
6
  end
data/lib/tapioca.rb CHANGED
@@ -43,11 +43,14 @@ module Tapioca
43
43
  DEFAULT_TODO_FILE = T.let("#{DEFAULT_RBI_DIR}/todo.rbi", String)
44
44
  DEFAULT_ANNOTATIONS_DIR = T.let("#{DEFAULT_RBI_DIR}/annotations", String)
45
45
 
46
- DEFAULT_OVERRIDES = T.let({
47
- # ActiveSupport overrides some core methods with different signatures
48
- # so we generate a typed: false RBI for it to suppress errors
49
- "activesupport" => "false",
50
- }.freeze, T::Hash[String, String])
46
+ DEFAULT_OVERRIDES = T.let(
47
+ {
48
+ # ActiveSupport overrides some core methods with different signatures
49
+ # so we generate a typed: false RBI for it to suppress errors
50
+ "activesupport" => "false",
51
+ }.freeze,
52
+ T::Hash[String, String],
53
+ )
51
54
 
52
55
  DEFAULT_RBI_MAX_LINE_LENGTH = 120
53
56
  DEFAULT_ENVIRONMENT = "development"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tapioca
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.4
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ufuk Kayserilioglu
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: exe
13
13
  cert_chain: []
14
- date: 2022-12-19 00:00:00.000000000 Z
14
+ date: 2023-02-21 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: bundler
@@ -176,6 +176,7 @@ files:
176
176
  - lib/tapioca/dsl/compilers/active_record_fixtures.rb
177
177
  - lib/tapioca/dsl/compilers/active_record_relations.rb
178
178
  - lib/tapioca/dsl/compilers/active_record_scope.rb
179
+ - lib/tapioca/dsl/compilers/active_record_secure_token.rb
179
180
  - lib/tapioca/dsl/compilers/active_record_typed_store.rb
180
181
  - lib/tapioca/dsl/compilers/active_resource.rb
181
182
  - lib/tapioca/dsl/compilers/active_storage.rb
@@ -186,6 +187,7 @@ files:
186
187
  - lib/tapioca/dsl/compilers/graphql_input_object.rb
187
188
  - lib/tapioca/dsl/compilers/graphql_mutation.rb
188
189
  - lib/tapioca/dsl/compilers/identity_cache.rb
190
+ - lib/tapioca/dsl/compilers/kredis.rb
189
191
  - lib/tapioca/dsl/compilers/mixed_in_class_attributes.rb
190
192
  - lib/tapioca/dsl/compilers/protobuf.rb
191
193
  - lib/tapioca/dsl/compilers/rails_generators.rb
@@ -195,6 +197,7 @@ files:
195
197
  - lib/tapioca/dsl/compilers/url_helpers.rb
196
198
  - lib/tapioca/dsl/extensions/active_record.rb
197
199
  - lib/tapioca/dsl/extensions/frozen_record.rb
200
+ - lib/tapioca/dsl/extensions/kredis.rb
198
201
  - lib/tapioca/dsl/helpers/active_record_column_type_helper.rb
199
202
  - lib/tapioca/dsl/helpers/active_record_constants_helper.rb
200
203
  - lib/tapioca/dsl/helpers/graphql_type_helper.rb
@@ -239,6 +242,8 @@ files:
239
242
  - lib/tapioca/rbi_ext/model.rb
240
243
  - lib/tapioca/rbi_formatter.rb
241
244
  - lib/tapioca/repo_index.rb
245
+ - lib/tapioca/runtime/attached_class_of_32.rb
246
+ - lib/tapioca/runtime/attached_class_of_legacy.rb
242
247
  - lib/tapioca/runtime/dynamic_mixin_compiler.rb
243
248
  - lib/tapioca/runtime/generic_type_registry.rb
244
249
  - lib/tapioca/runtime/reflection.rb