Module: T::Private::Methods

Defined in:
lib/types/private/methods/_methods.rb,
lib/types/private/methods/decl_builder.rb

Overview

typed: true

Defined Under Namespace

Modules: CallValidation, MethodHooks, Modes, SignatureValidation, SingletonMethodHooks Classes: DeclBuilder, Declaration, DeclarationBlock, Signature

Constant Summary collapse

ARG_NOT_PROVIDED =
Object.new
PROC_TYPE =
Object.new

Class Method Summary collapse

Class Method Details

._check_final_ancestors(target, target_ancestors, source_method_names) ⇒ Object

when target includes a module with instance methods source_method_names, ensure there is zero intersection between the final instance methods of target and source_method_names. so, for every m in source_method_names, check if there is already a method defined on one of target_ancestors with the same name that is final.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/types/private/methods/_methods.rb', line 133

def self._check_final_ancestors(target, target_ancestors, source_method_names)
  if !module_with_final?(target)
    return
  end
  # use reverse_each to check farther-up ancestors first, for better error messages. we could avoid this if we were on
  # the version of ruby that adds the optional argument to method_defined? that allows you to exclude ancestors.
  target_ancestors.reverse_each do |ancestor|
    source_method_names.each do |method_name|
      # the usage of method_owner_and_name_to_key(ancestor, method_name) instead of
      # method_to_key(ancestor.instance_method(method_name)) is not (just) an optimization, but also required for
      # correctness, since ancestor.method_defined?(method_name) may return true even if method_name is not defined
      # directly on ancestor but instead an ancestor of ancestor.
      if ancestor.method_defined?(method_name) && final_method?(method_owner_and_name_to_key(ancestor, method_name))
        raise(
          "The method `#{method_name}` on #{ancestor} was declared as final and cannot be " +
          (target == ancestor ? "redefined" : "overridden in #{target}")
        )
      end
    end
  end
end

._hook_impl(target, target_ancestors, source) ⇒ Object

the module target is adding the methods from the module source to itself. we need to check that for all instance methods M on source, M is not defined on any of target’s ancestors.



387
388
389
390
391
392
393
394
# File 'lib/types/private/methods/_methods.rb', line 387

def self._hook_impl(target, target_ancestors, source)
  if !module_with_final?(target) && !module_with_final?(source)
    return
  end
  add_module_with_final(target)
  install_hooks(target)
  _check_final_ancestors(target, target_ancestors - source.ancestors, source.instance_methods)
end

._on_method_added(hook_mod, method_name, is_singleton_method: false) ⇒ Object

Only public because it needs to get called below inside the replace_method blocks below.



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/types/private/methods/_methods.rb', line 173

def self._on_method_added(hook_mod, method_name, is_singleton_method: false)
  if T::Private::DeclState.current.skip_on_method_added
    return
  end

  current_declaration = T::Private::DeclState.current.active_declaration
  mod = is_singleton_method ? hook_mod.singleton_class : hook_mod

  if T::Private::Final.final_module?(mod) && (current_declaration.nil? || !current_declaration.final)
    raise "#{mod} was declared as final but its method `#{method_name}` was not declared as final"
  end
  _check_final_ancestors(mod, mod.ancestors, [method_name])

  if current_declaration.nil?
    return
  end
  T::Private::DeclState.current.reset!

  if method_name == :method_added || method_name == :singleton_method_added
    raise(
      "Putting a `sig` on `#{method_name}` is not supported" +
      " (sorbet-runtime uses this method internally to perform `sig` validation logic)"
    )
  end

  original_method = mod.instance_method(method_name)
  sig_block = lambda do
    T::Private::Methods.run_sig(hook_mod, method_name, original_method, current_declaration)
  end

  # Always replace the original method with this wrapper,
  # which is called only on the *first* invocation.
  # This wrapper is very slow, so it will subsequently re-wrap with a much faster wrapper
  # (or unwrap back to the original method).
  new_method = nil
  T::Private::ClassUtils.replace_method(mod, method_name) do |*args, &blk|
    if !T::Private::Methods.has_sig_block_for_method(new_method)
      # This should only happen if the user used alias_method to grab a handle
      # to the original pre-unwound `sig` method. I guess we'll just proxy the
      # call forever since we don't know who is holding onto this handle to
      # replace it.
      new_new_method = mod.instance_method(method_name)
      if new_method == new_new_method
        raise "`sig` not present for method `#{method_name}` but you're trying to run it anyways. " \
        "This should only be executed if you used `alias_method` to grab a handle to a method after `sig`ing it, but that clearly isn't what you are doing. " \
        "Maybe look to see if an exception was thrown in your `sig` lambda or somehow else your `sig` wasn't actually applied to the method. " \
        "Contact #dev-productivity if you're really stuck."
      end
      return new_new_method.bind(self).call(*args, &blk)
    end

    method_sig = T::Private::Methods.run_sig_block_for_method(new_method)

    # Should be the same logic as CallValidation.wrap_method_if_needed but we
    # don't want that extra layer of indirection in the callstack
    if method_sig.mode == T::Private::Methods::Modes.abstract
      # We're in an interface method, keep going up the chain
      if defined?(super)
        super(*args, &blk)
      else
        raise NotImplementedError.new("The method `#{method_sig.method_name}` on #{mod} is declared as `abstract`. It does not have an implementation.")
      end
    # Note, this logic is duplicated (intentionally, for micro-perf) at `CallValidation.wrap_method_if_needed`,
    # make sure to keep changes in sync.
    elsif method_sig.check_level == :always || (method_sig.check_level == :tests && T::Private::RuntimeLevels.check_tests?)
      CallValidation.validate_call(self, original_method, method_sig, args, blk)
    else
      original_method.bind(self).call(*args, &blk)
    end
  end

  new_method = mod.instance_method(method_name)
  key = method_to_key(new_method)
  @sig_wrappers[key] = sig_block
  if current_declaration.final
    add_final_method(key)
    # use hook_mod, not mod, because for example, we want class C to be marked as having final if we def C.foo as
    # final. change this to mod to see some final_method tests fail.
    add_module_with_final(hook_mod)
  end
end

.add_module_with_final(mod) ⇒ Object



163
164
165
166
# File 'lib/types/private/methods/_methods.rb', line 163

def self.add_module_with_final(mod)
  @modules_with_final.add(mod)
  @modules_with_final.add(mod.singleton_class)
end

.build_sig(hook_mod, method_name, original_method, current_declaration, loc) ⇒ Object



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/types/private/methods/_methods.rb', line 285

def self.build_sig(hook_mod, method_name, original_method, current_declaration, loc)
  begin
    # We allow `sig` in the current module's context (normal case) and inside `class << self`
    if hook_mod != current_declaration.mod && hook_mod.singleton_class != current_declaration.mod
      raise "A method (#{method_name}) is being added on a different class/module (#{hook_mod}) than the " \
            "last call to `sig` (#{current_declaration.mod}). Make sure each call " \
            "to `sig` is immediately followed by a method definition on the same " \
            "class/module."
    end

    signature = Signature.new(
      method: original_method,
      method_name: method_name,
      raw_arg_types: current_declaration.params,
      raw_return_type: current_declaration.returns,
      bind: current_declaration.bind,
      mode: current_declaration.mode,
      check_level: current_declaration.checked,
      on_failure: current_declaration.on_failure,
      override_allow_incompatible: current_declaration.override_allow_incompatible,
      generated: current_declaration.generated,
    )

    SignatureValidation.validate(signature)
    signature
  rescue => e
    super_method = original_method&.super_method
    super_signature = signature_for_method(super_method) if super_method

    T::Configuration.sig_validation_error_handler(
      e,
      method: original_method,
      declaration: current_declaration,
      signature: signature,
      super_signature: super_signature
    )

    Signature.new_untyped(method: original_method)
  end
end

.declare_sig(mod, arg, &blk) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/types/private/methods/_methods.rb', line 32

def self.declare_sig(mod, arg, &blk)
  install_hooks(mod)

  if T::Private::DeclState.current.active_declaration
    T::Private::DeclState.current.reset!
    raise "You called sig twice without declaring a method inbetween"
  end

  if !arg.nil? && arg != :final
    raise "Invalid argument to `sig`: #{arg}"
  end

  loc = caller_locations(2, 1).first

  T::Private::DeclState.current.active_declaration = DeclarationBlock.new(mod, loc, blk, arg == :final)

  nil
end

.finalize_proc(decl) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/types/private/methods/_methods.rb', line 55

def self.finalize_proc(decl)
  decl.finalized = true

  if decl.mode != Modes.standard
    raise "Procs cannot have override/abstract modifiers"
  end
  if decl.mod != PROC_TYPE
    raise "You are passing a DeclBuilder as a type. Did you accidentally use `self` inside a `sig` block?"
  end
  if decl.returns == ARG_NOT_PROVIDED
    raise "Procs must specify a return type"
  end
  if decl.on_failure != ARG_NOT_PROVIDED
    raise "Procs cannot use .on_failure"
  end

  if decl.params == ARG_NOT_PROVIDED
    decl.params = {}
  end

  T::Types::Proc.new(decl.params, decl.returns) # rubocop:disable PrisonGuard/UseOpusTypesShortcut
end

.has_sig_block_for_method(method) ⇒ Object



331
332
333
# File 'lib/types/private/methods/_methods.rb', line 331

def self.has_sig_block_for_method(method)
  has_sig_block_for_key(method_to_key(method))
end

.install_hooks(mod) ⇒ Object



435
436
437
438
439
440
441
442
443
444
445
# File 'lib/types/private/methods/_methods.rb', line 435

def self.install_hooks(mod)
  return if @installed_hooks.include?(mod)
  @installed_hooks << mod

  if mod.singleton_class?
    mod.include(SingletonMethodHooks)
  else
    mod.extend(MethodHooks)
  end
  mod.extend(SingletonMethodHooks)
end

.maybe_run_sig_block_for_method(method) ⇒ Object



339
340
341
# File 'lib/types/private/methods/_methods.rb', line 339

def self.maybe_run_sig_block_for_method(method)
  maybe_run_sig_block_for_key(method_to_key(method))
end

.register_forwarder(from_method, to_method, mode: Modes.overridable, remove_first_param: false) ⇒ Object

See docs at T::Utils.register_forwarder.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/types/private/methods/_methods.rb', line 79

def self.register_forwarder(from_method, to_method, mode: Modes.overridable, remove_first_param: false)
  # Normalize the method (see comment in signature_for_key).
  from_method = from_method.owner.instance_method(from_method.name)

  from_key = method_to_key(from_method)
  maybe_run_sig_block_for_key(from_key)
  if @signatures_by_method.key?(from_key)
    raise "#{from_method} already has a method signature"
  end

  from_params = from_method.parameters
  if from_params.length != 2 || from_params[0][0] != :rest || from_params[1][0] != :block
    raise "forwarder methods should take a single splat param and a block param. `#{from_method}` " \
          "takes these params: #{from_params}. For help, ask #dev-productivity."
  end

  # If there's already a signature for to_method, we get `parameters` from there, to enable
  # chained forwarding. NB: we don't use `signature_for_key` here, because the normalization it
  # does is broken when `to_method` has been clobbered by another method.
  to_key = method_to_key(to_method)
  maybe_run_sig_block_for_key(to_key)
  to_params = @signatures_by_method[to_key]&.parameters || to_method.parameters

  if remove_first_param
    to_params = to_params[1..-1]
  end

  # We don't bother trying to preserve any types from to_signature because this won't be
  # statically analyzed, and the types will just get checked when the forwarding happens.
  from_signature = Signature.new_untyped(method: from_method, mode: mode, parameters: to_params)
  @signatures_by_method[from_key] = from_signature
end

.run_all_sig_blocksObject



377
378
379
380
381
382
383
# File 'lib/types/private/methods/_methods.rb', line 377

def self.run_all_sig_blocks
  loop do
    break if @sig_wrappers.empty?
    key, _ = @sig_wrappers.first
    run_sig_block_for_key(key)
  end
end

.run_builder(declaration_block) ⇒ Object



277
278
279
280
281
282
283
# File 'lib/types/private/methods/_methods.rb', line 277

def self.run_builder(declaration_block)
  builder = DeclBuilder.new(declaration_block.mod)
  builder
    .instance_exec(&declaration_block.blk)
    .finalize!
    .decl
end

.run_sig(hook_mod, method_name, original_method, declaration_block) ⇒ Object

Executes the ‘sig` block, and converts the resulting Declaration to a Signature.



257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/types/private/methods/_methods.rb', line 257

def self.run_sig(hook_mod, method_name, original_method, declaration_block)
  current_declaration =
    begin
      run_builder(declaration_block)
    rescue DeclBuilder::BuilderError => e
      T::Configuration.sig_builder_error_handler(e, declaration_block.loc)
      nil
    end

  signature =
    if current_declaration
      build_sig(hook_mod, method_name, original_method, current_declaration, declaration_block.loc)
    else
      Signature.new_untyped(method: original_method)
    end

  unwrap_method(hook_mod, signature, original_method)
  signature
end

.run_sig_block_for_method(method) ⇒ Object



347
348
349
# File 'lib/types/private/methods/_methods.rb', line 347

def self.run_sig_block_for_method(method)
  run_sig_block_for_key(method_to_key(method))
end

.set_final_checks_on_hooks(enable) ⇒ Object



396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/types/private/methods/_methods.rb', line 396

def self.set_final_checks_on_hooks(enable)
  is_enabled = @old_hooks != nil
  if enable == is_enabled
    return
  end
  if is_enabled
    @old_hooks.each(&:restore)
    @old_hooks = nil
  else
    old_included = T::Private::ClassUtils.replace_method(Module, :included) do |arg|
      old_included.bind(self).call(arg)
      ::T::Private::Methods._hook_impl(arg, arg.ancestors, self)
    end
    old_extended = T::Private::ClassUtils.replace_method(Module, :extended) do |arg|
      old_extended.bind(self).call(arg)
      ::T::Private::Methods._hook_impl(arg, arg.singleton_class.ancestors, self)
    end
    old_inherited = T::Private::ClassUtils.replace_method(Class, :inherited) do |arg|
      old_inherited.bind(self).call(arg)
      ::T::Private::Methods._hook_impl(arg, arg.ancestors, self)
    end
    @old_hooks = [old_included, old_extended, old_inherited]
  end
end

.signature_for_method(method) ⇒ T::Private::Methods::Signature

Returns the signature for a method whose definition was preceded by ‘sig`.

Parameters:

  • method (UnboundMethod)

Returns:



116
117
118
# File 'lib/types/private/methods/_methods.rb', line 116

def self.signature_for_method(method)
  signature_for_key(method_to_key(method))
end

.start_procObject



51
52
53
# File 'lib/types/private/methods/_methods.rb', line 51

def self.start_proc
  DeclBuilder.new(PROC_TYPE)
end

.unwrap_method(hook_mod, signature, original_method) ⇒ Object



326
327
328
329
# File 'lib/types/private/methods/_methods.rb', line 326

def self.unwrap_method(hook_mod, signature, original_method)
  maybe_wrapped_method = CallValidation.wrap_method_if_needed(signature.method.owner, signature, original_method)
  @signatures_by_method[method_to_key(maybe_wrapped_method)] = signature
end