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

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



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

def self._on_method_added(hook_mod, method_name, is_singleton_method: false)
  current_declaration = T::Private::DeclState.current.active_declaration
  return if !current_declaration
  T::Private::DeclState.current.reset!

  mod = is_singleton_method ? hook_mod.singleton_class : hook_mod
  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)
  @sig_wrappers[method_to_key(new_method)] = sig_block
end

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



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

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,
      soft_notify: current_declaration.soft_notify,
      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, &blk) ⇒ Object



15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/types/private/methods/_methods.rb', line 15

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

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

  loc = caller_locations(2, 1).first

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

  nil
end

.finalize_proc(decl) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/types/private/methods/_methods.rb', line 34

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.soft_notify != ARG_NOT_PROVIDED
    raise "Procs cannot use .soft"
  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



255
256
257
# File 'lib/types/private/methods/_methods.rb', line 255

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

.install_hooks(mod) ⇒ Object



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/types/private/methods/_methods.rb', line 303

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

  if mod.singleton_class?
    install_singleton_method_added_hook(mod)
    install_singleton_method_added_hook(mod.singleton_class)
  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

    install_singleton_method_added_hook(mod.singleton_class)
  end
end

.maybe_run_sig_block_for_method(method) ⇒ Object



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

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.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/types/private/methods/_methods.rb', line 58

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



295
296
297
298
299
300
301
# File 'lib/types/private/methods/_methods.rb', line 295

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



197
198
199
200
201
202
203
# File 'lib/types/private/methods/_methods.rb', line 197

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.



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/types/private/methods/_methods.rb', line 177

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



271
272
273
# File 'lib/types/private/methods/_methods.rb', line 271

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

.sig_error(loc, message) ⇒ Object

Raises:

  • (ArgumentError)


167
168
169
170
171
172
173
# File 'lib/types/private/methods/_methods.rb', line 167

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:



95
96
97
# File 'lib/types/private/methods/_methods.rb', line 95

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

.start_procObject



30
31
32
# File 'lib/types/private/methods/_methods.rb', line 30

def self.start_proc
  DeclBuilder.new(PROC_TYPE)
end

.unwrap_method(hook_mod, signature, original_method) ⇒ Object



250
251
252
253
# File 'lib/types/private/methods/_methods.rb', line 250

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