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
TOP_SELF =

This has to be here, and can’t be nested inside ‘T::Private::Methods`, because the value of `self` depends on lexical (nesting) scope, and we specifically need a reference to the file-level self, i.e. `main:Object`

self

Class Method Summary collapse

Class Method Details

._check_final_ancestors(target, target_ancestors, source_method_names, source) ⇒ 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.

we assume that source_method_names has already been filtered to only include method names that were declared final at one point.



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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
# File 'lib/types/private/methods/_methods.rb', line 136

def self._check_final_ancestors(target, target_ancestors, source_method_names, source)
  source_ancestors = nil
  # use reverse_each to check farther-up ancestors first, for better error messages.
  target_ancestors.reverse_each do |ancestor|
    final_methods = @modules_with_final.fetch(ancestor, nil)
    # In this case, either ancestor didn't have any final methods anywhere in its
    # ancestor chain, or ancestor did have final methods somewhere in its ancestor
    # chain, but no final methods defined in ancestor itself.  Either way, there
    # are no final methods to check here, so we can move on to the next ancestor.
    next unless final_methods
    source_method_names.each do |method_name|
      next unless final_methods.include?(method_name)

      # If we get here, we are defining a method that some ancestor declared as
      # final.  however, we permit a final method to be defined multiple
      # times if it is the same final method being defined each time.
      if source
        if !source_ancestors
          source_ancestors = source.ancestors
          # filter out things without actual final methods just to make sure that
          # the below checks (which should be uncommon) go as quickly as possible.
          source_ancestors.select! do |a|
            @modules_with_final.fetch(a, nil)
          end
        end
        # final-ness means that there should be no more than one index for which
        # the below block returns true.
        defining_ancestor_idx = source_ancestors.index do |a|
          @modules_with_final.fetch(a).include?(method_name)
        end
        next if defining_ancestor_idx && source_ancestors[defining_ancestor_idx] == ancestor
      end

      definition_file, definition_line = T::Private::Methods.signature_for_method(ancestor.instance_method(method_name)).method.source_location
      is_redefined = target == ancestor
      caller_loc = T::Private::CallerUtils.find_caller {|loc| !loc.path.to_s.start_with?(SORBET_RUNTIME_LIB_PATH)}
      extra_info = "\n"
      if caller_loc
        extra_info = (is_redefined ? "Redefined" : "Overridden") + " here: #{caller_loc.path}:#{caller_loc.lineno}\n"
      end

      error_message = "The method `#{method_name}` on #{ancestor} was declared as final and cannot be " +
                      (is_redefined ? "redefined" : "overridden in #{target}")
      pretty_message = "#{error_message}\n" \
                       "Made final here: #{definition_file}:#{definition_line}\n" \
                       "#{extra_info}"

      begin
        raise pretty_message
      rescue => e
        # sig_validation_error_handler raises by default; on the off chance that
        # it doesn't raise, we need to ensure that the rest of signature building
        # sees a consistent state.  This sig failed to validate, so we should get
        # rid of it.  If we don't do this, errors of the form "You called sig
        # twice without declaring a method in between" will non-deterministically
        # crop up in tests.
        T::Private::DeclState.current.reset!
        T::Configuration.sig_validation_error_handler(e, {})
      end
    end
  end
end

._declare_sig(mod, arg = nil, &blk) ⇒ Object

See tests for how to use this. But you shouldn’t be using this.



46
47
48
# File 'lib/types/private/methods/_methods.rb', line 46

def self._declare_sig(mod, arg=nil, &blk)
  _declare_sig_internal(mod, caller_locations(1, 1).first, arg, raw: true, &blk)
end

._handle_missing_method_signature(receiver, original_method, callee) ⇒ Object



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
325
326
327
328
329
330
331
# File 'lib/types/private/methods/_methods.rb', line 296

def self._handle_missing_method_signature(receiver, original_method, callee)
  method_sig = T::Private::Methods.signature_for_method(original_method)
  if !method_sig
    raise "`sig` not present for method `#{callee}` on #{receiver.inspect} 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."
  end

  if receiver.class <= original_method.owner
    receiving_class = receiver.class
  elsif receiver.singleton_class <= original_method.owner
    receiving_class = receiver.singleton_class
  elsif receiver.is_a?(Module) && receiver <= original_method.owner
    receiving_class = receiver
  else
    raise "#{receiver} is not related to #{original_method} - how did we get here?"
  end

  # Check for a case where `alias` or `alias_method` was called for a
  # method which had already had a `sig` applied. In that case, we want
  # to avoid hitting this slow path again, by moving to a faster validator
  # just like we did or will for the original method.
  #
  # If this isn't an `alias` or `alias_method` case, we're probably in the
  # middle of some metaprogramming using a Method object, e.g. a pattern like
  # `arr.map(&method(:foo))`. There's nothing really we can do to optimize
  # that here.
  receiving_method = receiving_class.instance_method(callee)
  if receiving_method != original_method && receiving_method.original_name == original_method.name
    aliasing_mod = receiving_method.owner
    method_sig = method_sig.as_alias(callee)
    unwrap_method(aliasing_mod, method_sig, original_method)
  end

  method_sig
end

._hook_impl(target, singleton_class, 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.



479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/types/private/methods/_methods.rb', line 479

def self._hook_impl(target, singleton_class, source)
  # we do not need to call add_was_ever_final here, because we have already marked
  # any such methods when source was originally defined.
  if !@modules_with_final.include?(target)
    if !@modules_with_final.include?(source)
      return
    end
    note_module_deals_with_final(target)
    install_hooks(target)
    return
  end

  methods = source.instance_methods
  methods.select! do |method_name|
    @was_ever_final_names.include?(method_name)
  end
  if methods.empty?
    return
  end

  target_ancestors = singleton_class ? target.singleton_class.ancestors : target.ancestors
  _check_final_ancestors(target, target_ancestors, methods, source)
end

._on_method_added(hook_mod, mod, method_name) ⇒ Object

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



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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/types/private/methods/_methods.rb', line 216

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

  current_declaration = T::Private::DeclState.current.active_declaration

  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
  # Don't compute mod.ancestors if we don't need to bother checking final-ness.
  if @was_ever_final_names.include?(method_name) && @modules_with_final.include?(mod)
    _check_final_ancestors(mod, mod.ancestors, [method_name], nil)
    # We need to fetch the active declaration again, as _check_final_ancestors
    # may have reset it (see the comment in that method for details).
    current_declaration = T::Private::DeclState.current.active_declaration
  end

  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).
  key = method_owner_and_name_to_key(mod, method_name)
  unless current_declaration.raw
    T::Private::ClassUtils.replace_method(mod, method_name, true) do |*args, &blk|
      method_sig = T::Private::Methods.maybe_run_sig_block_for_key(key)
      method_sig ||= T::Private::Methods._handle_missing_method_signature(
        self,
        original_method,
        __callee__,
      )

      # 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)
      elsif T::Configuration::AT_LEAST_RUBY_2_7
        original_method.bind_call(self, *args, &blk)
      else
        original_method.bind(self).call(*args, &blk) # rubocop:disable Performance/BindCall
      end
    end
  end

  @sig_wrappers[key] = sig_block
  if current_declaration.final
    @was_ever_final_names[method_name] = true
    # 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.
    note_module_deals_with_final(hook_mod)
    add_module_with_final_method(mod, method_name)
  end
end

._with_declared_signature(mod, declblock, &blk) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/types/private/methods/_methods.rb', line 65

def self._with_declared_signature(mod, declblock, &blk)
  # If declblock is provided, this code is equivalent to the check in
  # _declare_sig_internal, above.
  # If declblock is not provided and we have an active declaration, we are
  # obviously doing something wrong.
  if T::Private::DeclState.current.active_declaration
    T::Private::DeclState.current.reset!
    raise "You called sig twice without declaring a method in between"
  end
  if declblock
    T::Private::DeclState.current.active_declaration = declblock
  end
  mod.module_exec(&blk)
end

.add_module_with_final_method(mod, method_name) ⇒ Object



199
200
201
202
203
204
205
206
207
# File 'lib/types/private/methods/_methods.rb', line 199

def self.add_module_with_final_method(mod, method_name)
  methods = @modules_with_final[mod]
  if methods.nil?
    methods = {}
    @modules_with_final[mod] = methods
  end
  methods[method_name] = true
  nil
end

.all_checked_tests_sigsObject



473
474
475
# File 'lib/types/private/methods/_methods.rb', line 473

def self.all_checked_tests_sigs
  @signatures_by_method.values.select {|sig| sig.check_level == :tests}
end

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



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/types/private/methods/_methods.rb', line 365

def self.build_sig(hook_mod, method_name, original_method, current_declaration)
  begin
    # We allow `sig` in the current module's context (normal case) and
    if hook_mod != current_declaration.mod &&
       # inside `class << self`, and
       hook_mod.singleton_class != current_declaration.mod &&
       # on `self` at the top level of a file
       current_declaration.mod != TOP_SELF
      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,
      defined_raw: current_declaration.raw,
    )

    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, loc, arg, &blk) ⇒ Object



39
40
41
42
43
# File 'lib/types/private/methods/_methods.rb', line 39

def self.declare_sig(mod, loc, arg, &blk)
  T::Private::DeclState.current.active_declaration = _declare_sig_internal(mod, loc, arg, &blk)

  nil
end

.finalize_proc(decl) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/types/private/methods/_methods.rb', line 84

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? Perhaps you wanted the `T.self_type` instead: https://sorbet.org/docs/self-type"
  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)
end

.has_sig_block_for_method(method) ⇒ Object



415
416
417
# File 'lib/types/private/methods/_methods.rb', line 415

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

.install_hooks(mod) ⇒ Object



560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# File 'lib/types/private/methods/_methods.rb', line 560

def self.install_hooks(mod)
  return if @installed_hooks.include?(mod)
  @installed_hooks[mod] = true

  if mod == TOP_SELF
    # self at the top-level of a file is weirdly special in Ruby
    # The Ruby VM on startup creates an `Object.new` and stashes it.
    # Unlike when we're using sig inside a module, `self` is actually a
    # normal object, not an instance of Module.
    #
    # Thus we can't ask things like mod.singleton_class? (since that's
    # defined only on Module, not on Object) and even if we could, the places
    # where we need to install the hooks are special.
    mod.extend(SingletonMethodHooks) # def self.foo; end (at top level)
    Object.extend(MethodHooks)       # def foo; end      (at top level)
    return
  end

  # See https://github.com/sorbet/sorbet/pull/3964 for an explanation of why this
  # check (which theoretically should not be needed) is actually needed.
  if !mod.is_a?(Module)
    return
  end

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

.maybe_run_sig_block_for_key(key) ⇒ Object

Only public so that it can be accessed in the closure for _on_method_added



428
429
430
# File 'lib/types/private/methods/_methods.rb', line 428

def self.maybe_run_sig_block_for_key(key)
  run_sig_block_for_key(key) if has_sig_block_for_key(key)
end

.maybe_run_sig_block_for_method(method) ⇒ Object



423
424
425
# File 'lib/types/private/methods/_methods.rb', line 423

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

.note_module_deals_with_final(mod) ⇒ Object



209
210
211
212
213
# File 'lib/types/private/methods/_methods.rb', line 209

def self.note_module_deals_with_final(mod)
  # Side-effectfully initialize the value if it's not already there
  @modules_with_final[mod]
  @modules_with_final[mod.singleton_class]
end

.run_all_sig_blocks(force_type_init: true) ⇒ Object



465
466
467
468
469
470
471
# File 'lib/types/private/methods/_methods.rb', line 465

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

.run_builder(declaration_block) ⇒ Object



357
358
359
360
361
362
363
# File 'lib/types/private/methods/_methods.rb', line 357

def self.run_builder(declaration_block)
  builder = DeclBuilder.new(declaration_block.mod, declaration_block.raw)
  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.



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/types/private/methods/_methods.rb', line 335

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

  declaration_block.loc = nil

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

  unwrap_method(signature.method.owner, signature, original_method)
  signature
end

.run_sig_block_for_method(method) ⇒ Object



432
433
434
# File 'lib/types/private/methods/_methods.rb', line 432

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



503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/types/private/methods/_methods.rb', line 503

def self.set_final_checks_on_hooks(enable)
  is_enabled = !@old_hooks.nil?
  if enable == is_enabled
    return
  end
  if is_enabled
    # A cut-down version of T::Private::ClassUtils::ReplacedMethod#restore, because we
    # should only be resetting final hooks during tests.
    T::Configuration.without_ruby_warnings do
      Module.define_method(:included, @old_hooks[0])
      Module.define_method(:extended, @old_hooks[1])
      Class.define_method(:inherited, @old_hooks[2])
    end
    @old_hooks = nil
  else
    old_included = T::Private::ClassUtils.replace_method(Module, :included, true) do |arg|
      if T::Configuration::AT_LEAST_RUBY_2_7
        old_included.bind_call(self, arg)
      else
        old_included.bind(self).call(arg) # rubocop:disable Performance/BindCall
      end
      ::T::Private::Methods._hook_impl(arg, false, self)
    end
    old_extended = T::Private::ClassUtils.replace_method(Module, :extended, true) do |arg|
      if T::Configuration::AT_LEAST_RUBY_2_7
        old_extended.bind_call(self, arg)
      else
        old_extended.bind(self).call(arg) # rubocop:disable Performance/BindCall
      end
      ::T::Private::Methods._hook_impl(arg, true, self)
    end
    old_inherited = T::Private::ClassUtils.replace_method(Class, :inherited, true) do |arg|
      if T::Configuration::AT_LEAST_RUBY_2_7
        old_inherited.bind_call(self, arg)
      else
        old_inherited.bind(self).call(arg) # rubocop:disable Performance/BindCall
      end
      ::T::Private::Methods._hook_impl(arg, false, 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:



111
112
113
# File 'lib/types/private/methods/_methods.rb', line 111

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

.start_procObject



80
81
82
# File 'lib/types/private/methods/_methods.rb', line 80

def self.start_proc
  DeclBuilder.new(PROC_TYPE, false)
end

.unwrap_method(mod, signature, original_method) ⇒ Object



410
411
412
413
# File 'lib/types/private/methods/_methods.rb', line 410

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