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



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/types/private/methods/_methods.rb', line 125

def self._check_final_ancestors(target, target_ancestors, source_method_names)
  # 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(
          "`#{ancestor.name}##{method_name}` was declared as final and cannot be " +
          (target == ancestor ? "redefined" : "overridden in `#{target.name}`")
        )
      end
    end
  end
end

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



369
370
371
372
373
374
375
376
# File 'lib/types/private/methods/_methods.rb', line 369

def self._included_extended_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.



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
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
# File 'lib/types/private/methods/_methods.rb', line 161

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

  current_declaration = T::Private::DeclState.current.active_declaration
  mod = is_singleton_method ? hook_mod.singleton_class : hook_mod
  original_method = mod.instance_method(method_name)

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

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

  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
  # this prevents us from running the final checks twice for every method def.
  T::Private::DeclState.current.skip_next_on_method_added = true
  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)
    add_module_with_final(mod)
  end
end

.add_module_with_final(mod) ⇒ Object



152
153
154
# File 'lib/types/private/methods/_methods.rb', line 152

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

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



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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/types/private/methods/_methods.rb', line 269

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

    if current_declaration.returns.equal?(ARG_NOT_PROVIDED)
      sig_error(loc, "You must provide a return type; use the `.returns` or `.void` builder methods. Method: #{original_method}")
    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::Private::ErrorHandler.handle_sig_validation_error(
      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



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/types/private/methods/_methods.rb', line 24

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



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/types/private/methods/_methods.rb', line 47

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



319
320
321
# File 'lib/types/private/methods/_methods.rb', line 319

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

.install_hooks(mod) ⇒ Object



399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/types/private/methods/_methods.rb', line 399

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

  if mod.singleton_class?
    install_singleton_method_added_hook(mod)
  else
    original_method = T::Private::ClassUtils.replace_method(mod.singleton_class, :method_added) do |name|
      T::Private::Methods._on_method_added(self, name)
      original_method.bind(self).call(name)
    end
  end
  install_singleton_method_added_hook(mod.singleton_class)
end

.maybe_run_sig_block_for_method(method) ⇒ Object



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

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.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/types/private/methods/_methods.rb', line 71

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



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

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



261
262
263
264
265
266
267
# File 'lib/types/private/methods/_methods.rb', line 261

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.



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/types/private/methods/_methods.rb', line 241

def self.run_sig(hook_mod, method_name, original_method, declaration_block)
  current_declaration =
    begin
      run_builder(declaration_block)
    rescue DeclBuilder::BuilderError => e
      T::Private::ErrorHandler.handle_sig_builder_error(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



335
336
337
# File 'lib/types/private/methods/_methods.rb', line 335

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

.set_final_checks_for_include_extend(enable) ⇒ Object



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/types/private/methods/_methods.rb', line 378

def self.set_final_checks_for_include_extend(enable)
  is_enabled = @old_included_extended != nil
  if enable == is_enabled
    return
  end
  if is_enabled
    @old_included_extended.each(&:restore)
    @old_included_extended = nil
  else
    old_included = T::Private::ClassUtils.replace_method(Module, :included) do |arg|
      old_included.bind(self).call(arg)
      ::T::Private::Methods._included_extended_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._included_extended_impl(arg, arg.singleton_class.ancestors, self)
    end
    @old_included_extended = [old_included, old_extended]
  end
end

.sig_error(loc, message) ⇒ Object

Raises:

  • (ArgumentError)


235
236
237
# File 'lib/types/private/methods/_methods.rb', line 235

def self.sig_error(loc, message)
  raise(ArgumentError.new("#{loc.path}:#{loc.lineno}: Error interpreting `sig`:\n  #{message}\n\n"))
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:



108
109
110
# File 'lib/types/private/methods/_methods.rb', line 108

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

.start_procObject



43
44
45
# File 'lib/types/private/methods/_methods.rb', line 43

def self.start_proc
  DeclBuilder.new(PROC_TYPE)
end

.unwrap_method(hook_mod, signature, original_method) ⇒ Object



314
315
316
317
# File 'lib/types/private/methods/_methods.rb', line 314

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