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: ARG_NOT_PROVIDED, CallValidation, MethodHooks, Modes, PROC_TYPE, SignatureValidation, SingletonMethodHooks Classes: DeclBuilder, Declaration, DeclarationBlock, Signature

Constant Summary collapse

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, 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.



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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
# File 'lib/types/private/methods/_methods.rb', line 114

def self._check_final_ancestors(target, source_method_names, source)
  source_ancestors = nil
  if T::Private::IS_TYPECHECKING
    # Need to avoid a pinning error, but don't want to use runtime types in _methods.rb
    source_ancestors = T.let(nil, T.nilable(T::Array[T::Module[T.anything]]))
  end
  # 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

      final_sig = T::Private::Methods.signature_for_method(ancestor.instance_method(method_name))
      definition_file, definition_line = final_sig&.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

._handle_missing_method_signature(receiver, original_method, callee) ⇒ Object



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
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/types/private/methods/_methods.rb', line 270

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, 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.



494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
# File 'lib/types/private/methods/_methods.rb', line 494

def self._hook_impl(target, 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
    # Side-effectfully initialize the value if it's not already there
    @modules_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

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



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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/types/private/methods/_methods.rb', line 196

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, [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(original_method, mod, method_name) 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__ || raise("Unknown __callee__ for method without a signature"),
      )

      # 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_call(self, *args, &blk)
      end
    end
  end

  @sig_wrappers[key] = sig_block
  if current_declaration.final
    add_module_with_final_method(mod, method_name)
  end
end

.add_module_with_final_method(mod, method_name) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/types/private/methods/_methods.rb', line 182

def self.add_module_with_final_method(mod, method_name)
  @was_ever_final_names[method_name] = true

  # Side-effectfully initializes the value if it's not already there
  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



488
489
490
# File 'lib/types/private/methods/_methods.rb', line 488

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



353
354
355
356
357
358
359
360
361
362
363
364
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
# File 'lib/types/private/methods/_methods.rb', line 353

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



53
54
55
56
57
# File 'lib/types/private/methods/_methods.rb', line 53

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



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/types/private/methods/_methods.rb', line 78

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



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

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

.install_hooks(mod) ⇒ Object



570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
# File 'lib/types/private/methods/_methods.rb', line 570

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



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

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



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

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

.run_all_sig_blocks(force_type_init: true) ⇒ Object



479
480
481
482
483
484
485
486
# File 'lib/types/private/methods/_methods.rb', line 479

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

.run_builder(declaration_block) ⇒ Object



339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/types/private/methods/_methods.rb', line 339

def self.run_builder(declaration_block)
  blk_or_decl = declaration_block.blk_or_decl
  return blk_or_decl if blk_or_decl.is_a?(Declaration)
  if blk_or_decl.nil?
    raise "DeclarationBlock for #{declaration_block.mod} at #{declaration_block.loc} should have already been unwrapped"
  end

  builder = DeclBuilder.new(declaration_block.mod, declaration_block.raw)
  decl = builder.instance_exec(&blk_or_decl).finalize!.decl
  # Record that we've already run `blk` once and constructed a `Declaration`
  declaration_block.blk_or_decl = decl
  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.



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/types/private/methods/_methods.rb', line 309

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

  # Release location information sooner
  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)

  # Drop this declaration. Only drop it after we've actually wrapped the
  # method and recorded the signature, because that might raise an exception,
  # leaving the declaration in a weird state if the program rescues that
  # exception and continues.
  declaration_block.blk_or_decl = nil

  signature
end

.run_sig_block_for_method(method) ⇒ Object



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

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



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
545
546
547
548
549
550
551
552
553
554
# File 'lib/types/private/methods/_methods.rb', line 518

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
    # Grab the original methods before replacing them, so that each block
    # closure can reference a variable that is already assigned.
    # (Do this directly, to avoid pinning errors)
    old_included = Module.instance_method(:included)
    T::Private::ClassUtils.replace_method(old_included, Module, :included) do |arg|
      old_included.bind_call(self, arg)
      ::T::Private::Methods._hook_impl(arg, self)
    end
    old_extended = Module.instance_method(:extended)
    T::Private::ClassUtils.replace_method(old_extended, Module, :extended) do |arg|
      old_extended.bind_call(self, arg)
      ::T::Private::Methods._hook_impl(arg.singleton_class, self)
    end
    old_inherited = Class.instance_method(:inherited)
    T::Private::ClassUtils.replace_method(old_inherited, Class, :inherited) do |arg|
      old_inherited.bind_call(self, arg)
      ::T::Private::Methods._hook_impl(arg, self)
      ::T::Private::Methods._hook_impl(arg.singleton_class, self.singleton_class)
    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:



402
403
404
# File 'lib/types/private/methods/_methods.rb', line 402

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

.start_procObject



74
75
76
# File 'lib/types/private/methods/_methods.rb', line 74

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