tapioca 0.16.4 → 0.16.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80c01f44b0600c3a471756da73e43322f23a951c8f3ebc15c922173a0ccf4d3c
4
- data.tar.gz: c778cfa78b0494513f5ad329e432c534486c3ba75fcf83969462a874c7dc7654
3
+ metadata.gz: 5a42a478b0396f43dd9e6505c18ac69ca8c32e6ef00b298e6d6e4a8c12536b17
4
+ data.tar.gz: f24ab38ee1d78f1633f7d580ea5be8f93e13c9c6550d11b5d5bc4171d974929b
5
5
  SHA512:
6
- metadata.gz: f12a7fbe0d64109a5de03d8a98f564413bee79dc0db1ced8c1eaeae8dae73e68c73e8e76a188a06ddfefa333b11024d606359b7f706d13e15dacea081b0a19ea
7
- data.tar.gz: 99ef8194c8dab5e776b591992aefa8b59b0b8e2d8927caecfc68d63bf7bb4c7687a92af772c6f8ac5fcfbdd8ebe1f9c1b748a2334b8207d233939d35d02d2b26
6
+ metadata.gz: f623d3b5e66a081728d9ae4b3dde4c6c976829ffbd6e606a64def2e8aff489c09911396ca72146c901f72a11d432d6c86ae840e278b8d85fd8ee756e97776542
7
+ data.tar.gz: e7686bcfed28858bfb8f162ee0bd8a7c9a6cdf286c7227a86032025934b04fa72c29f21c41d7a3c5668edb8aad2d39bf2808a2d98799a7434e94aa9b921c3cf8
data/README.md CHANGED
@@ -489,8 +489,7 @@ Options:
489
489
  # Default: false
490
490
  -q, [--quiet], [--no-quiet], [--skip-quiet] # Suppresses file creation output
491
491
  # Default: false
492
- -w, [--workers=N] # Number of parallel workers to use when generating RBIs (default: 2)
493
- # Default: 2
492
+ -w, [--workers=N] # Number of parallel workers to use when generating RBIs (default: auto)
494
493
  [--rbi-max-line-length=N] # Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped
495
494
  # Default: 120
496
495
  -e, [--environment=ENVIRONMENT] # The Rack/Rails environment to use when generating RBIs
@@ -949,7 +948,7 @@ dsl:
949
948
  exclude: []
950
949
  verify: false
951
950
  quiet: false
952
- workers: 2
951
+ workers: 1
953
952
  rbi_max_line_length: 120
954
953
  environment: development
955
954
  list_compilers: false
@@ -1000,7 +999,16 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopif
1000
999
 
1001
1000
  ### DSL compilers
1002
1001
 
1003
- Contributions to existing DSL compilers are welcome. However, new compilers that support DSLs for gems other than Rails should live outside of Tapioca. Please refer to [writing custom dsl compilers](https://github.com/Shopify/tapioca?tab=readme-ov-file#writing-custom-dsl-compilers) for more information.
1002
+ Tapioca ships with a small collection of high quality DSL compilers for popular Ruby gems that are used heavily at Shopify, like Rails and GraphQL. We encourage the community to contribute new DSL compilers, though they shouldn't necessarily live in the Tapioca repo itself.
1003
+
1004
+ It's best for DSL compilers to be contributed directly to gems they apply to ([example](https://github.com/Shopify/measured/tree/main/lib/tapioca/dsl/compilers)). This way, when changes are made to the gem's DSL, the gem's DSL compiler can be updated at the same time and be versioned/released together.
1005
+
1006
+ If an upstream gem's maintainers don't wish to host a DSL compiler themselves, you can propose contributing it to:
1007
+
1008
+ 1. Tapioca, if it's a gem that Shopify uses (ask us in an issue or PR)
1009
+ 2. A third party DSL compiler repository, like [AngelList/Boba](https://github.com/angellist/boba). These are not supported by Shopify.
1010
+
1011
+ For help writing a DSL compiler, please refer to [writing custom dsl compilers](https://github.com/Shopify/tapioca?tab=readme-ov-file#writing-custom-dsl-compilers).
1004
1012
 
1005
1013
  ## License
1006
1014
 
data/exe/tapioca CHANGED
@@ -20,6 +20,11 @@ unless ENV["ENFORCE_TYPECHECKING"] == "1"
20
20
  end
21
21
  end
22
22
 
23
+ unless defined?(Bundler)
24
+ puts "Warning: You're running tapioca without Bundler. This isn't recommended and may cause issues. " \
25
+ "Please use the provided binstub through `bin/tapioca` instead."
26
+ end
27
+
23
28
  require_relative "../lib/tapioca/internal"
24
29
 
25
30
  Tapioca::Cli.start(ARGV)
@@ -0,0 +1,132 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ RubyLsp::Addon.depend_on_ruby_lsp!(">= 0.22.1", "< 0.23")
5
+
6
+ begin
7
+ # The Tapioca add-on depends on the Rails add-on to add a runtime component to the runtime server. We can allow the
8
+ # add-on to work outside of a Rails context in the future, but that may require Tapioca spawning its own runtime
9
+ # server
10
+ require "ruby_lsp/ruby_lsp_rails/runner_client"
11
+ rescue LoadError
12
+ return
13
+ end
14
+
15
+ require "zlib"
16
+
17
+ module RubyLsp
18
+ module Tapioca
19
+ class Addon < ::RubyLsp::Addon
20
+ extend T::Sig
21
+
22
+ sig { void }
23
+ def initialize
24
+ super
25
+
26
+ @global_state = T.let(nil, T.nilable(RubyLsp::GlobalState))
27
+ @rails_runner_client = T.let(nil, T.nilable(RubyLsp::Rails::RunnerClient))
28
+ @index = T.let(nil, T.nilable(RubyIndexer::Index))
29
+ @file_checksums = T.let({}, T::Hash[String, String])
30
+ @outgoing_queue = T.let(nil, T.nilable(Thread::Queue))
31
+ end
32
+
33
+ sig { override.params(global_state: RubyLsp::GlobalState, outgoing_queue: Thread::Queue).void }
34
+ def activate(global_state, outgoing_queue)
35
+ @global_state = global_state
36
+ return unless @global_state.enabled_feature?(:tapiocaAddon)
37
+
38
+ @index = @global_state.index
39
+ @outgoing_queue = outgoing_queue
40
+ Thread.new do
41
+ # Get a handle to the Rails add-on's runtime client. The call to `rails_runner_client` will block this thread
42
+ # until the server has finished booting, but it will not block the main LSP. This has to happen inside of a
43
+ # thread
44
+ addon = T.cast(::RubyLsp::Addon.get("Ruby LSP Rails", ">= 0.3.17", "< 0.4"), ::RubyLsp::Rails::Addon)
45
+ @rails_runner_client = addon.rails_runner_client
46
+ @outgoing_queue << Notification.window_log_message("Activating Tapioca add-on v#{version}")
47
+ @rails_runner_client.register_server_addon(File.expand_path("server_addon.rb", __dir__))
48
+ rescue IncompatibleApiError
49
+ # The requested version for the Rails add-on no longer matches. We need to upgrade and fix the breaking
50
+ # changes
51
+ @outgoing_queue << Notification.window_log_message(
52
+ "IncompatibleApiError: Cannot activate Tapioca LSP add-on",
53
+ type: Constant::MessageType::WARNING,
54
+ )
55
+ end
56
+ end
57
+
58
+ sig { override.void }
59
+ def deactivate
60
+ end
61
+
62
+ sig { override.returns(String) }
63
+ def name
64
+ "Tapioca"
65
+ end
66
+
67
+ sig { override.returns(String) }
68
+ def version
69
+ "0.1.0"
70
+ end
71
+
72
+ sig { params(changes: T::Array[{ uri: String, type: Integer }]).void }
73
+ def workspace_did_change_watched_files(changes)
74
+ return unless T.must(@global_state).enabled_feature?(:tapiocaAddon)
75
+ return unless @rails_runner_client # Client is not ready
76
+
77
+ constants = changes.flat_map do |change|
78
+ path = URI(change[:uri]).to_standardized_path
79
+ next if path.end_with?("_test.rb", "_spec.rb")
80
+ next unless file_updated?(change, path)
81
+
82
+ entries = T.must(@index).entries_for(path)
83
+ next unless entries
84
+
85
+ entries.filter_map do |entry|
86
+ entry.name if entry.class == RubyIndexer::Entry::Class || entry.class == RubyIndexer::Entry::Module
87
+ end
88
+ end.compact
89
+
90
+ return if constants.empty?
91
+
92
+ @rails_runner_client.trigger_reload
93
+ @rails_runner_client.delegate_notification(
94
+ server_addon_name: "Tapioca",
95
+ request_name: "dsl",
96
+ constants: constants,
97
+ )
98
+ end
99
+
100
+ private
101
+
102
+ sig { params(change: T::Hash[Symbol, T.untyped], path: String).returns(T::Boolean) }
103
+ def file_updated?(change, path)
104
+ case change[:type]
105
+ when Constant::FileChangeType::CREATED
106
+ @file_checksums[path] = Zlib.crc32(File.read(path)).to_s
107
+ return true
108
+ when Constant::FileChangeType::CHANGED
109
+ current_checksum = Zlib.crc32(File.read(path)).to_s
110
+ if @file_checksums[path] == current_checksum
111
+ T.must(@outgoing_queue) << Notification.window_log_message(
112
+ "File has not changed. Skipping #{path}",
113
+ type: Constant::MessageType::INFO,
114
+ )
115
+ else
116
+ @file_checksums[path] = current_checksum
117
+ return true
118
+ end
119
+ when Constant::FileChangeType::DELETED
120
+ @file_checksums.delete(path)
121
+ else
122
+ T.must(@outgoing_queue) << Notification.window_log_message(
123
+ "Unexpected file change type: #{change[:type]}",
124
+ type: Constant::MessageType::WARNING,
125
+ )
126
+ end
127
+
128
+ false
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,30 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "tapioca/internal"
5
+
6
+ module RubyLsp
7
+ module Tapioca
8
+ class ServerAddon < ::RubyLsp::Rails::ServerAddon
9
+ def name
10
+ "Tapioca"
11
+ end
12
+
13
+ def execute(request, params)
14
+ case request
15
+ when "dsl"
16
+ fork do
17
+ dsl(params)
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def dsl(params)
25
+ load("tapioca/cli.rb") # Reload the CLI to reset thor defaults between requests
26
+ ::Tapioca::Cli.start(["dsl", "--lsp_addon", "--workers=1"] + params[:constants])
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/tapioca/cli.rb CHANGED
@@ -111,8 +111,7 @@ module Tapioca
111
111
  option :workers,
112
112
  aliases: ["-w"],
113
113
  type: :numeric,
114
- desc: "Number of parallel workers to use when generating RBIs (default: 2)",
115
- default: 2
114
+ desc: "Number of parallel workers to use when generating RBIs (default: auto)"
116
115
  option :rbi_max_line_length,
117
116
  type: :numeric,
118
117
  desc: "Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped",
@@ -144,6 +143,11 @@ module Tapioca
144
143
  type: :hash,
145
144
  desc: "Options to pass to the DSL compilers",
146
145
  default: {}
146
+ option :lsp_addon,
147
+ type: :boolean,
148
+ desc: "Generate DSL RBIs from the LSP add-on. Internal to Tapioca and not intended for end-users",
149
+ default: false,
150
+ hide: true
147
151
  def dsl(*constant_or_paths)
148
152
  set_environment(options)
149
153
 
@@ -166,6 +170,7 @@ module Tapioca
166
170
  app_root: options[:app_root],
167
171
  halt_upon_load_error: options[:halt_upon_load_error],
168
172
  compiler_options: options[:compiler_options],
173
+ lsp_addon: options[:lsp_addon],
169
174
  }
170
175
 
171
176
  command = if options[:verify]
@@ -261,6 +266,11 @@ module Tapioca
261
266
  type: :boolean,
262
267
  desc: "Halt upon a load error while loading the Rails application",
263
268
  default: true
269
+ option :lsp_addon,
270
+ type: :boolean,
271
+ desc: "Generate Gem RBIs from the LSP add-on. Internal to Tapioca and not intended for end-users",
272
+ default: false,
273
+ hide: true
264
274
  def gem(*gems)
265
275
  set_environment(options)
266
276
 
@@ -295,6 +305,7 @@ module Tapioca
295
305
  dsl_dir: options[:dsl_dir],
296
306
  rbi_formatter: rbi_formatter(options),
297
307
  halt_upon_load_error: options[:halt_upon_load_error],
308
+ lsp_addon: options[:lsp_addon],
298
309
  }
299
310
 
300
311
  command = if verify
@@ -28,6 +28,7 @@ module Tapioca
28
28
  app_root: String,
29
29
  halt_upon_load_error: T::Boolean,
30
30
  compiler_options: T::Hash[String, T.untyped],
31
+ lsp_addon: T::Boolean,
31
32
  ).void
32
33
  end
33
34
  def initialize(
@@ -47,7 +48,8 @@ module Tapioca
47
48
  rbi_formatter: DEFAULT_RBI_FORMATTER,
48
49
  app_root: ".",
49
50
  halt_upon_load_error: true,
50
- compiler_options: {}
51
+ compiler_options: {},
52
+ lsp_addon: false
51
53
  )
52
54
  @requested_constants = requested_constants
53
55
  @requested_paths = requested_paths
@@ -66,6 +68,7 @@ module Tapioca
66
68
  @halt_upon_load_error = halt_upon_load_error
67
69
  @skip_constant = skip_constant
68
70
  @compiler_options = compiler_options
71
+ @lsp_addon = lsp_addon
69
72
 
70
73
  super()
71
74
  end
@@ -74,6 +77,10 @@ module Tapioca
74
77
 
75
78
  sig { params(outpath: Pathname, quiet: T::Boolean).returns(T::Set[Pathname]) }
76
79
  def generate_dsl_rbi_files(outpath, quiet:)
80
+ if @lsp_addon
81
+ pipeline.active_compilers.each(&:reset_state)
82
+ end
83
+
77
84
  existing_rbi_files = existing_rbi_filenames(all_requested_constants)
78
85
 
79
86
  generated_files = pipeline.run do |constant, contents|
@@ -116,6 +123,7 @@ module Tapioca
116
123
  eager_load: @requested_constants.empty? && @requested_paths.empty?,
117
124
  app_root: @app_root,
118
125
  halt_upon_load_error: @halt_upon_load_error,
126
+ lsp_addon: @lsp_addon,
119
127
  )
120
128
  end
121
129
 
@@ -133,6 +141,7 @@ module Tapioca
133
141
  skipped_constants: constantize(@skip_constant, ignore_missing: true),
134
142
  number_of_workers: @number_of_workers,
135
143
  compiler_options: @compiler_options,
144
+ lsp_addon: @lsp_addon,
136
145
  )
137
146
  end
138
147
 
@@ -170,7 +179,9 @@ module Tapioca
170
179
  raise Thor::Error, ""
171
180
  end
172
181
 
173
- processable_constants.map { |_, constant| constant }
182
+ processable_constants
183
+ .map { |_, constant| constant }
184
+ .grep(Module)
174
185
  end
175
186
 
176
187
  sig { params(compiler_names: T::Array[String]).returns(T::Array[T.class_of(Tapioca::Dsl::Compiler)]) }
@@ -27,6 +27,7 @@ module Tapioca
27
27
  dsl_dir: String,
28
28
  rbi_formatter: RBIFormatter,
29
29
  halt_upon_load_error: T::Boolean,
30
+ lsp_addon: T.nilable(T::Boolean),
30
31
  ).void
31
32
  end
32
33
  def initialize(
@@ -45,7 +46,8 @@ module Tapioca
45
46
  auto_strictness: true,
46
47
  dsl_dir: DEFAULT_DSL_DIR,
47
48
  rbi_formatter: DEFAULT_RBI_FORMATTER,
48
- halt_upon_load_error: true
49
+ halt_upon_load_error: true,
50
+ lsp_addon: false
49
51
  )
50
52
  @gem_names = gem_names
51
53
  @exclude = exclude
@@ -59,6 +61,7 @@ module Tapioca
59
61
  @auto_strictness = auto_strictness
60
62
  @dsl_dir = dsl_dir
61
63
  @rbi_formatter = rbi_formatter
64
+ @lsp_addon = lsp_addon
62
65
 
63
66
  super()
64
67
 
@@ -73,39 +76,6 @@ module Tapioca
73
76
 
74
77
  private
75
78
 
76
- sig { params(gem_names: T::Array[String]).returns(T::Array[Gemfile::GemSpec]) }
77
- def gems_to_generate(gem_names)
78
- return @bundle.dependencies if gem_names.empty?
79
-
80
- gem_names.each_with_object([]) do |gem_name, gems|
81
- gem = @bundle.gem(gem_name)
82
-
83
- if gem.nil?
84
- raise Thor::Error, set_color("Error: Cannot find gem '#{gem_name}'", :red)
85
- end
86
-
87
- gems.concat(gem_dependencies(gem)) if @include_dependencies
88
- gems << gem
89
- end
90
- end
91
-
92
- sig do
93
- params(
94
- gem: Gemfile::GemSpec,
95
- dependencies: T::Array[Gemfile::GemSpec],
96
- ).returns(T::Array[Gemfile::GemSpec])
97
- end
98
- def gem_dependencies(gem, dependencies = [])
99
- direct_dependencies = gem.dependencies.filter_map { |dependency| @bundle.gem(dependency.name) }
100
- gems = dependencies | direct_dependencies
101
-
102
- if direct_dependencies.empty?
103
- gems
104
- else
105
- direct_dependencies.reduce(gems) { |result, gem| gem_dependencies(gem, result) }
106
- end
107
- end
108
-
109
79
  sig { params(gem: Gemfile::GemSpec).void }
110
80
  def compile_gem_rbi(gem)
111
81
  gem_name = set_color(gem.name, :yellow, :bold)
@@ -19,7 +19,7 @@ module Tapioca
19
19
  purge_stale_dsl_rbi_files(rbi_files_to_purge)
20
20
  say("Done", :green)
21
21
 
22
- if @auto_strictness
22
+ if @auto_strictness && !@lsp_addon
23
23
  say("")
24
24
  validate_rbi_files(
25
25
  command: default_command(:dsl, all_requested_constants.join(" ")),
@@ -46,6 +46,41 @@ module Tapioca
46
46
  ensure
47
47
  GitAttributes.create_generated_attribute_file(@outpath)
48
48
  end
49
+
50
+ sig { params(gem_names: T::Array[String]).returns(T::Array[Gemfile::GemSpec]) }
51
+ def gems_to_generate(gem_names)
52
+ return @bundle.dependencies if gem_names.empty?
53
+
54
+ (gem_names - @exclude).each_with_object([]) do |gem_name, gems|
55
+ gem = @bundle.gem(gem_name)
56
+
57
+ if gem.nil?
58
+ next if @lsp_addon
59
+
60
+ raise Thor::Error, set_color("Error: Cannot find gem '#{gem_name}'", :red)
61
+ end
62
+
63
+ gems.concat(gem_dependencies(gem)) if @include_dependencies
64
+ gems << gem
65
+ end
66
+ end
67
+
68
+ sig do
69
+ params(
70
+ gem: Gemfile::GemSpec,
71
+ dependencies: T::Array[Gemfile::GemSpec],
72
+ ).returns(T::Array[Gemfile::GemSpec])
73
+ end
74
+ def gem_dependencies(gem, dependencies = [])
75
+ direct_dependencies = gem.dependencies.filter_map { |dependency| @bundle.gem(dependency.name) }
76
+ gems = dependencies | direct_dependencies
77
+
78
+ if direct_dependencies.empty?
79
+ gems
80
+ else
81
+ direct_dependencies.reduce(gems) { |result, gem| gem_dependencies(gem, result) }
82
+ end
83
+ end
49
84
  end
50
85
  end
51
86
  end
@@ -51,6 +51,13 @@ module Tapioca
51
51
  @@requested_constants = constants # rubocop:disable Style/ClassVars
52
52
  end
53
53
 
54
+ sig { void }
55
+ def reset_state
56
+ @processable_constants = nil
57
+ @all_classes = nil
58
+ @all_modules = nil
59
+ end
60
+
54
61
  private
55
62
 
56
63
  sig do
@@ -74,11 +81,7 @@ module Tapioca
74
81
  sig { returns(T::Enumerable[T::Class[T.anything]]) }
75
82
  def all_classes
76
83
  @all_classes ||= T.let(
77
- if @@requested_constants.any?
78
- @@requested_constants.grep(Class)
79
- else
80
- ObjectSpace.each_object(Class)
81
- end,
84
+ all_modules.grep(Class).freeze,
82
85
  T.nilable(T::Enumerable[T::Class[T.anything]]),
83
86
  )
84
87
  end
@@ -87,10 +90,10 @@ module Tapioca
87
90
  def all_modules
88
91
  @all_modules ||= T.let(
89
92
  if @@requested_constants.any?
90
- @@requested_constants.select { |k| k.is_a?(Module) }
93
+ @@requested_constants.grep(Module)
91
94
  else
92
- ObjectSpace.each_object(Module)
93
- end,
95
+ ObjectSpace.each_object(Module).to_a
96
+ end.freeze,
94
97
  T.nilable(T::Enumerable[Module]),
95
98
  )
96
99
  end
@@ -101,26 +101,26 @@ module Tapioca
101
101
 
102
102
  sig { params(attribute_type_value: T.untyped).returns(::String) }
103
103
  def type_for(attribute_type_value)
104
- type = case attribute_type_value
104
+ case attribute_type_value
105
105
  when ActiveModel::Type::Boolean
106
- "T::Boolean"
106
+ as_nilable_type("T::Boolean")
107
107
  when ActiveModel::Type::Date
108
- "::Date"
108
+ as_nilable_type("::Date")
109
109
  when ActiveModel::Type::DateTime, ActiveModel::Type::Time
110
- "::Time"
110
+ as_nilable_type("::Time")
111
111
  when ActiveModel::Type::Decimal
112
- "::BigDecimal"
112
+ as_nilable_type("::BigDecimal")
113
113
  when ActiveModel::Type::Float
114
- "::Float"
114
+ as_nilable_type("::Float")
115
115
  when ActiveModel::Type::Integer
116
- "::Integer"
116
+ as_nilable_type("::Integer")
117
117
  when ActiveModel::Type::String
118
- "::String"
118
+ as_nilable_type("::String")
119
119
  else
120
- Helpers::ActiveModelTypeHelper.type_for(attribute_type_value)
120
+ type = Helpers::ActiveModelTypeHelper.type_for(attribute_type_value)
121
+ type = as_nilable_type(type) if Helpers::ActiveModelTypeHelper.assume_nilable?(attribute_type_value)
122
+ type
121
123
  end
122
-
123
- as_nilable_type(type)
124
124
  end
125
125
 
126
126
  sig { params(klass: RBI::Scope, method: String, type: String).void }
@@ -210,10 +210,6 @@ module Tapioca
210
210
  query_methods |= ActiveRecord::SpawnMethods.instance_methods(false)
211
211
  # Remove the ones we know are private API
212
212
  query_methods -= [:all, :arel, :build_subquery, :construct_join_dependency, :extensions, :spawn]
213
- # Remove "group" which needs a custom return type for GroupChains
214
- query_methods -= [:group]
215
- # Remove "where" which needs a custom return type for WhereChains
216
- query_methods -= [:where]
217
213
  # Remove the methods that ...
218
214
  query_methods
219
215
  .grep_v(/_clause$/) # end with "_clause"
@@ -376,7 +372,7 @@ module Tapioca
376
372
  return_type: "T.self_type",
377
373
  )
378
374
 
379
- CALCULATION_METHODS.each do |method_name|
375
+ (CALCULATION_METHODS + [:size]).each do |method_name|
380
376
  case method_name
381
377
  when :average, :maximum, :minimum
382
378
  klass.create_method(
@@ -404,9 +400,9 @@ module Tapioca
404
400
  ],
405
401
  return_type: "T::Hash[T.untyped, Integer]",
406
402
  )
407
- when :sum
403
+ when :sum, :size
408
404
  klass.create_method(
409
- "sum",
405
+ method_name.to_s,
410
406
  parameters: [
411
407
  create_opt_param("column_name", type: "T.nilable(T.any(String, Symbol))", default: "nil"),
412
408
  create_block_param("block", type: "T.nilable(T.proc.params(record: T.untyped).returns(T.untyped))"),
@@ -419,7 +415,7 @@ module Tapioca
419
415
 
420
416
  sig { void }
421
417
  def create_relation_where_chain_class
422
- model.create_class(RelationWhereChainClassName, superclass_name: RelationClassName) do |klass|
418
+ model.create_class(RelationWhereChainClassName) do |klass|
423
419
  create_where_chain_methods(klass, RelationClassName)
424
420
  klass.create_type_variable("Elem", type: "type_member", fixed: constant_name)
425
421
  end
@@ -427,10 +423,7 @@ module Tapioca
427
423
 
428
424
  sig { void }
429
425
  def create_association_relation_where_chain_class
430
- model.create_class(
431
- AssociationRelationWhereChainClassName,
432
- superclass_name: AssociationRelationClassName,
433
- ) do |klass|
426
+ model.create_class(AssociationRelationWhereChainClassName) do |klass|
434
427
  create_where_chain_methods(klass, AssociationRelationClassName)
435
428
  klass.create_type_variable("Elem", type: "type_member", fixed: constant_name)
436
429
  end
@@ -560,27 +553,21 @@ module Tapioca
560
553
  sig { void }
561
554
  def create_relation_methods
562
555
  create_relation_method("all")
563
- create_relation_method(
564
- "group",
565
- parameters: [
566
- create_rest_param("args", type: "T.untyped"),
567
- create_block_param("blk", type: "T.untyped"),
568
- ],
569
- relation_return_type: RelationGroupChainClassName,
570
- association_return_type: AssociationRelationGroupChainClassName,
571
- )
572
- create_relation_method(
573
- "where",
574
- parameters: [
575
- create_rest_param("args", type: "T.untyped"),
576
- create_block_param("blk", type: "T.untyped"),
577
- ],
578
- relation_return_type: RelationWhereChainClassName,
579
- association_return_type: AssociationRelationWhereChainClassName,
580
- )
581
556
 
582
557
  QUERY_METHODS.each do |method_name|
583
558
  case method_name
559
+ when :where
560
+ create_where_relation_method
561
+ when :group
562
+ create_relation_method(
563
+ "group",
564
+ parameters: [
565
+ create_rest_param("args", type: "T.untyped"),
566
+ create_block_param("blk", type: "T.untyped"),
567
+ ],
568
+ relation_return_type: RelationGroupChainClassName,
569
+ association_return_type: AssociationRelationGroupChainClassName,
570
+ )
584
571
  when :distinct
585
572
  create_relation_method(
586
573
  method_name.to_s,
@@ -599,6 +586,22 @@ module Tapioca
599
586
  parameters: parameters,
600
587
  return_type: return_type,
601
588
  )
589
+ when :select
590
+ [relation_methods_module, association_relation_methods_module].each do |mod|
591
+ mod.create_method(method_name.to_s) do |method|
592
+ method.add_rest_param("args")
593
+ method.add_block_param("blk")
594
+
595
+ method.add_sig do |sig|
596
+ sig.add_param("args", "T.untyped")
597
+ sig.return_type = mod == relation_methods_module ? RelationClassName : AssociationRelationClassName
598
+ end
599
+ method.add_sig do |sig|
600
+ sig.add_param("blk", "T.proc.params(record: #{constant_name}).returns(T::Boolean)")
601
+ sig.return_type = "T::Array[#{constant_name}]"
602
+ end
603
+ end
604
+ end
602
605
  else
603
606
  create_relation_method(
604
607
  method_name,
@@ -1056,6 +1059,35 @@ module Tapioca
1056
1059
  )
1057
1060
  end
1058
1061
 
1062
+ sig { void }
1063
+ def create_where_relation_method
1064
+ relation_methods_module.create_method("where") do |method|
1065
+ method.add_rest_param("args")
1066
+
1067
+ method.add_sig do |sig|
1068
+ sig.return_type = RelationWhereChainClassName
1069
+ end
1070
+
1071
+ method.add_sig do |sig|
1072
+ sig.add_param("args", "T.untyped")
1073
+ sig.return_type = RelationClassName
1074
+ end
1075
+ end
1076
+
1077
+ association_relation_methods_module.create_method("where") do |method|
1078
+ method.add_rest_param("args")
1079
+
1080
+ method.add_sig do |sig|
1081
+ sig.return_type = AssociationRelationWhereChainClassName
1082
+ end
1083
+
1084
+ method.add_sig do |sig|
1085
+ sig.add_param("args", "T.untyped")
1086
+ sig.return_type = AssociationRelationClassName
1087
+ end
1088
+ end
1089
+ end
1090
+
1059
1091
  sig do
1060
1092
  params(
1061
1093
  name: T.any(Symbol, String),
@@ -147,7 +147,17 @@ module Tapioca
147
147
  return "T.untyped" if type.nil?
148
148
 
149
149
  sorbet_type = if type.respond_to?(:sorbet_type)
150
+ line, file = type.method(:sorbet_type).source_location
151
+
152
+ $stderr.puts <<~MESSAGE
153
+ WARNING: `#sorbet_type` is deprecated. Please rename your method to `#__tapioca_type`."
154
+
155
+ Defined on line #{line} of #{file}
156
+ MESSAGE
157
+
150
158
  type.sorbet_type
159
+ elsif type.respond_to?(:__tapioca_type)
160
+ type.__tapioca_type
151
161
  elsif type == ::JsonApiClient::Schema::Types::Integer
152
162
  "::Integer"
153
163
  elsif type == ::JsonApiClient::Schema::Types::String
@@ -14,7 +14,8 @@ module Tapioca
14
14
  def type_for(type_value)
15
15
  return "T.untyped" if Runtime::GenericTypeRegistry.generic_type_instance?(type_value)
16
16
 
17
- type = lookup_return_type_of_method(type_value, :deserialize) ||
17
+ type = lookup_tapioca_type(type_value) ||
18
+ lookup_return_type_of_method(type_value, :deserialize) ||
18
19
  lookup_return_type_of_method(type_value, :cast) ||
19
20
  lookup_return_type_of_method(type_value, :cast_value) ||
20
21
  lookup_arg_type_of_method(type_value, :serialize) ||
@@ -22,6 +23,11 @@ module Tapioca
22
23
  type.to_s
23
24
  end
24
25
 
26
+ sig { params(type_value: T.untyped).returns(T::Boolean) }
27
+ def assume_nilable?(type_value)
28
+ !type_value.respond_to?(:__tapioca_type)
29
+ end
30
+
25
31
  private
26
32
 
27
33
  MEANINGLESS_TYPES = T.let(
@@ -39,6 +45,11 @@ module Tapioca
39
45
  !MEANINGLESS_TYPES.include?(type)
40
46
  end
41
47
 
48
+ sig { params(obj: T.untyped).returns(T.nilable(T::Types::Base)) }
49
+ def lookup_tapioca_type(obj)
50
+ T::Utils.coerce(obj.__tapioca_type) if obj.respond_to?(:__tapioca_type)
51
+ end
52
+
42
53
  sig { params(obj: T.untyped, method: Symbol).returns(T.nilable(T::Types::Base)) }
43
54
  def lookup_return_type_of_method(obj, method)
44
55
  return_type = lookup_signature_of_method(obj, method)&.return_type
@@ -211,6 +211,11 @@ module Tapioca
211
211
  ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Range === type
212
212
  }
213
213
  "T::Range[#{type_for_activerecord_value(column_type.subtype, column_nullability:)}]"
214
+ when ->(type) {
215
+ defined?(ActiveRecord::Locking::LockingType) &&
216
+ ActiveRecord::Locking::LockingType === type
217
+ }
218
+ as_non_nilable_if_persisted_and_not_nullable("::Integer", column_nullability:)
214
219
  else
215
220
  as_non_nilable_if_persisted_and_not_nullable(
216
221
  ActiveModelTypeHelper.type_for(column_type),
@@ -34,6 +34,7 @@ module Tapioca
34
34
  skipped_constants: T::Array[Module],
35
35
  number_of_workers: T.nilable(Integer),
36
36
  compiler_options: T::Hash[String, T.untyped],
37
+ lsp_addon: T::Boolean,
37
38
  ).void
38
39
  end
39
40
  def initialize(
@@ -44,7 +45,8 @@ module Tapioca
44
45
  error_handler: $stderr.method(:puts).to_proc,
45
46
  skipped_constants: [],
46
47
  number_of_workers: nil,
47
- compiler_options: {}
48
+ compiler_options: {},
49
+ lsp_addon: false
48
50
  )
49
51
  @active_compilers = T.let(
50
52
  gather_active_compilers(requested_compilers, excluded_compilers),
@@ -56,6 +58,7 @@ module Tapioca
56
58
  @skipped_constants = skipped_constants
57
59
  @number_of_workers = number_of_workers
58
60
  @compiler_options = compiler_options
61
+ @lsp_addon = lsp_addon
59
62
  @errors = T.let([], T::Array[String])
60
63
  end
61
64
 
@@ -177,7 +180,7 @@ module Tapioca
177
180
  # Find the constants that have been reloaded
178
181
  reloaded_constants = constants_by_name.select { |_, constants| constants.size > 1 }.keys
179
182
 
180
- unless reloaded_constants.empty?
183
+ unless reloaded_constants.empty? || @lsp_addon
181
184
  reloaded_constant_names = reloaded_constants.map { |name| "`#{name}`" }.join(", ")
182
185
 
183
186
  $stderr.puts("WARNING: Multiple constants with the same name: #{reloaded_constant_names}")
@@ -222,9 +225,14 @@ module Tapioca
222
225
 
223
226
  sig { void }
224
227
  def abort_if_pending_migrations!
228
+ # When running within the add-on, we cannot invoke the abort if pending migrations task because that will exit
229
+ # the process and crash the Rails runtime server. Instead, the Rails add-on checks for pending migrations and
230
+ # warns the user, so that they are aware they need to migrate their database
231
+ return if @lsp_addon
225
232
  return unless defined?(::Rake)
226
233
 
227
234
  Rails.application.load_tasks
235
+
228
236
  if Rake::Task.task_defined?("db:abort_if_pending_migrations")
229
237
  Rake::Task["db:abort_if_pending_migrations"].invoke
230
238
  end
@@ -66,15 +66,24 @@ module Tapioca
66
66
 
67
67
  sig { returns([T::Enumerable[Spec], T::Array[String]]) }
68
68
  def materialize_deps
69
- deps = definition.locked_gems.dependencies.values
70
- materialized_dependencies = definition.resolve.materialize(deps)
71
- missing_spec_names = materialized_dependencies.missing_specs.map(&:name).to_set
72
- missing_specs = materialized_dependencies.missing_specs.map do |spec|
73
- "#{spec.name} (#{spec.version})"
74
- end
75
- materialized_dependencies = materialized_dependencies.to_a.reject do |spec|
76
- missing_spec_names.include?(spec.name)
69
+ deps = definition.locked_gems.dependencies.except(*@excluded_gems).values
70
+ resolve = definition.resolve
71
+ materialized_dependencies = resolve.materialize(deps)
72
+
73
+ if Bundler::VERSION >= "2.6.0"
74
+ missing_specs = resolve.missing_specs.map do |spec|
75
+ "#{spec.name} (#{spec.version})"
76
+ end
77
+ else
78
+ missing_spec_names = materialized_dependencies.missing_specs.map(&:name).to_set
79
+ missing_specs = materialized_dependencies.missing_specs.map do |spec|
80
+ "#{spec.name} (#{spec.version})"
81
+ end
82
+ materialized_dependencies = materialized_dependencies.to_a.reject do |spec|
83
+ missing_spec_names.include?(spec.name)
84
+ end
77
85
  end
86
+
78
87
  [materialized_dependencies, missing_specs]
79
88
  end
80
89
 
@@ -28,7 +28,8 @@ class GitAttributes
28
28
  # exist, we just return.
29
29
  return unless path.exist?
30
30
 
31
- File.write(path.join(".gitattributes"), content)
31
+ git_attributes_path = path.join(".gitattributes")
32
+ File.write(git_attributes_path, content) unless git_attributes_path.exist?
32
33
  end
33
34
  end
34
35
  end
@@ -25,9 +25,6 @@ module URI
25
25
  # have the uri gem in their own bundle and thus not use a compatible version.
26
26
  PARSER = T.let(const_defined?(:RFC2396_PARSER) ? RFC2396_PARSER : DEFAULT_PARSER, RFC2396_Parser)
27
27
 
28
- alias_method(:gem_name, :host)
29
- alias_method(:line_number, :fragment)
30
-
31
28
  sig { returns(T.nilable(String)) }
32
29
  attr_reader :gem_version
33
30
 
@@ -54,6 +51,16 @@ module URI
54
51
  end
55
52
  end
56
53
 
54
+ sig { returns(T.nilable(String)) }
55
+ def gem_name
56
+ host
57
+ end
58
+
59
+ sig { returns(T.nilable(String)) }
60
+ def line_number
61
+ fragment
62
+ end
63
+
57
64
  sig { params(v: T.nilable(String)).void }
58
65
  def set_path(v) # rubocop:disable Naming/AccessorMethodName
59
66
  return if v.nil?
@@ -10,16 +10,32 @@ module Tapioca
10
10
  extend T::Sig
11
11
 
12
12
  sig do
13
- params(tapioca_path: String, eager_load: T::Boolean, app_root: String, halt_upon_load_error: T::Boolean).void
13
+ params(
14
+ tapioca_path: String,
15
+ eager_load: T::Boolean,
16
+ app_root: String,
17
+ halt_upon_load_error: T::Boolean,
18
+ lsp_addon: T::Boolean,
19
+ ).void
14
20
  end
15
- def load_application(tapioca_path:, eager_load: true, app_root: ".", halt_upon_load_error: true)
21
+ def load_application(
22
+ tapioca_path:,
23
+ eager_load: true,
24
+ app_root: ".",
25
+ halt_upon_load_error: true,
26
+ lsp_addon: false
27
+ )
16
28
  loader = new(
17
29
  tapioca_path: tapioca_path,
18
30
  eager_load: eager_load,
19
31
  app_root: app_root,
20
32
  halt_upon_load_error: halt_upon_load_error,
21
33
  )
22
- loader.load
34
+ if lsp_addon
35
+ loader.load_dsl_extensions_and_compilers
36
+ else
37
+ loader.load
38
+ end
23
39
  end
24
40
  end
25
41
 
@@ -30,6 +46,12 @@ module Tapioca
30
46
  load_dsl_compilers
31
47
  end
32
48
 
49
+ sig { void }
50
+ def load_dsl_extensions_and_compilers
51
+ load_dsl_extensions
52
+ load_dsl_compilers
53
+ end
54
+
33
55
  protected
34
56
 
35
57
  sig do
@@ -63,7 +85,7 @@ module Tapioca
63
85
  def load_dsl_compilers
64
86
  say("Loading DSL compiler classes... ")
65
87
 
66
- # Load all built-in compilers
88
+ # Load all built-in compilers before any custom compilers
67
89
  Dir.glob("#{Tapioca::LIB_ROOT_DIR}/tapioca/dsl/compilers/*.rb").each do |compiler|
68
90
  require File.expand_path(compiler)
69
91
  end
@@ -51,6 +51,8 @@ module Tapioca
51
51
 
52
52
  sig { params(paths: T::Array[Pathname]).returns(T::Set[String]) }
53
53
  def symbols_from_paths(paths)
54
+ return Set.new if paths.empty?
55
+
54
56
  output = Tempfile.create("sorbet") do |file|
55
57
  file.write(Array(paths).join("\n"))
56
58
  file.flush
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
- VERSION = "0.16.4"
5
+ VERSION = "0.16.6"
6
6
  end
metadata CHANGED
@@ -1,17 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tapioca
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.4
4
+ version: 0.16.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ufuk Kayserilioglu
8
8
  - Alan Wu
9
9
  - Alexandre Terrasa
10
10
  - Peter Zhu
11
- autorequire:
12
11
  bindir: exe
13
12
  cert_chain: []
14
- date: 2024-11-07 00:00:00.000000000 Z
13
+ date: 2025-01-06 00:00:00.000000000 Z
15
14
  dependencies:
16
15
  - !ruby/object:Gem::Dependency
17
16
  name: bundler
@@ -125,7 +124,6 @@ dependencies:
125
124
  - - ">="
126
125
  - !ruby/object:Gem::Version
127
126
  version: '0'
128
- description:
129
127
  email:
130
128
  - ruby@shopify.com
131
129
  executables:
@@ -136,6 +134,8 @@ files:
136
134
  - LICENSE.txt
137
135
  - README.md
138
136
  - exe/tapioca
137
+ - lib/ruby_lsp/tapioca/addon.rb
138
+ - lib/ruby_lsp/tapioca/server_addon.rb
139
139
  - lib/tapioca.rb
140
140
  - lib/tapioca/bundler_ext/auto_require_hook.rb
141
141
  - lib/tapioca/cli.rb
@@ -269,7 +269,6 @@ licenses:
269
269
  - MIT
270
270
  metadata:
271
271
  allowed_push_host: https://rubygems.org
272
- post_install_message:
273
272
  rdoc_options: []
274
273
  require_paths:
275
274
  - lib
@@ -284,8 +283,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
284
283
  - !ruby/object:Gem::Version
285
284
  version: '0'
286
285
  requirements: []
287
- rubygems_version: 3.5.23
288
- signing_key:
286
+ rubygems_version: 3.6.2
289
287
  specification_version: 4
290
288
  summary: A Ruby Interface file generator for gems, core types and the Ruby standard
291
289
  library