mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
Replace sorbet-runtime-stub with sorbet-runtime
This commit is contained in:
parent
30fda551e8
commit
e4264c9f35
3
.gitignore
vendored
3
.gitignore
vendored
@ -150,8 +150,7 @@
|
||||
**/vendor/bundle/ruby/*/gems/simplecov-*/
|
||||
**/vendor/bundle/ruby/*/gems/simplecov-html-*/
|
||||
**/vendor/bundle/ruby/*/gems/sorbet-*/
|
||||
**/vendor/bundle/ruby/*/gems/sorbet-runtime-*/
|
||||
!**/vendor/bundle/ruby/*/gems/sorbet-runtime-stub-*/
|
||||
!**/vendor/bundle/ruby/*/gems/sorbet-runtime-*/
|
||||
**/vendor/bundle/ruby/*/gems/spoom-*/
|
||||
**/vendor/bundle/ruby/*/gems/stackprof-*/
|
||||
**/vendor/bundle/ruby/*/gems/strscan-*/
|
||||
|
@ -17,6 +17,7 @@ gem "rspec-github", require: false
|
||||
gem "rspec-its", require: false
|
||||
gem "rspec_junit_formatter", require: false
|
||||
gem "rspec-retry", require: false
|
||||
gem "rspec-sorbet", require: false
|
||||
gem "rspec-wait", require: false
|
||||
gem "rubocop", require: false
|
||||
gem "rubocop-ast", require: false
|
||||
@ -26,7 +27,6 @@ gem "warning", require: false
|
||||
|
||||
group :sorbet, optional: true do
|
||||
gem "parlour", require: false
|
||||
gem "rspec-sorbet", require: false
|
||||
gem "sorbet-static-and-runtime", require: false
|
||||
gem "tapioca", require: false
|
||||
end
|
||||
@ -44,4 +44,4 @@ gem "rubocop-rails"
|
||||
gem "rubocop-rspec"
|
||||
gem "rubocop-sorbet"
|
||||
gem "ruby-macho"
|
||||
gem "sorbet-runtime-stub"
|
||||
gem "sorbet-runtime"
|
||||
|
@ -162,7 +162,6 @@ GEM
|
||||
sorbet (0.5.10160)
|
||||
sorbet-static (= 0.5.10160)
|
||||
sorbet-runtime (0.5.10160)
|
||||
sorbet-runtime-stub (0.2.0)
|
||||
sorbet-static (0.5.10160-universal-darwin-14)
|
||||
sorbet-static-and-runtime (0.5.10160)
|
||||
sorbet (= 0.5.10160)
|
||||
@ -235,7 +234,7 @@ DEPENDENCIES
|
||||
ruby-macho
|
||||
simplecov
|
||||
simplecov-cobertura
|
||||
sorbet-runtime-stub
|
||||
sorbet-runtime
|
||||
sorbet-static-and-runtime
|
||||
tapioca
|
||||
warning
|
||||
|
@ -88,7 +88,7 @@ module Homebrew
|
||||
def tests
|
||||
args = tests_args.parse
|
||||
|
||||
Homebrew.install_bundler_gems!(groups: ["sorbet"])
|
||||
Homebrew.install_bundler_gems!
|
||||
|
||||
require "byebug" if args.byebug?
|
||||
|
||||
|
@ -1,11 +1,47 @@
|
||||
# typed: true
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Explicitly prevent `sorbet-runtime` from being loaded.
|
||||
def gem(name, *)
|
||||
raise Gem::LoadError if name == "sorbet-runtime"
|
||||
require "sorbet-runtime"
|
||||
|
||||
super
|
||||
# Disable runtime checking unless enabled.
|
||||
# In the future we should consider not doing this monkey patch,
|
||||
# if assured that there is no performance hit from removing this.
|
||||
# There are mechanisms to achieve a middle ground (`default_checked_level`).
|
||||
unless ENV["HOMEBREW_SORBET_RUNTIME"]
|
||||
# Redefine T.let etc to make the `checked` parameter default to false rather than true.
|
||||
# @private
|
||||
module TNoChecks
|
||||
def cast(value, type, checked: false)
|
||||
super(value, type, checked: checked)
|
||||
end
|
||||
|
||||
require "sorbet-runtime-stub"
|
||||
def let(value, type, checked: false)
|
||||
super(value, type, checked: checked)
|
||||
end
|
||||
|
||||
def bind(value, type, checked: false)
|
||||
super(value, type, checked: checked)
|
||||
end
|
||||
|
||||
def assert_type!(value, type, checked: false)
|
||||
super(value, type, checked: checked)
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
module T
|
||||
class << self
|
||||
prepend TNoChecks
|
||||
end
|
||||
|
||||
# Redefine T.sig to be noop.
|
||||
# @private
|
||||
module Sig
|
||||
def sig(arg0 = nil, &blk); end
|
||||
end
|
||||
end
|
||||
|
||||
# For any cases the above doesn't handle: make sure we don't let TypeError slip through.
|
||||
T::Configuration.call_validation_error_handler = ->(signature, opts) do end
|
||||
T::Configuration.inline_type_error_handler = ->(error, opts) do end
|
||||
end
|
||||
|
@ -7,4 +7,4 @@ require_relative "standalone/load_path"
|
||||
require_relative "startup/ruby_path"
|
||||
require "startup/config"
|
||||
require_relative "startup/bootsnap"
|
||||
require_relative "startup/sorbet"
|
||||
require_relative "standalone/sorbet"
|
||||
|
@ -1,10 +0,0 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
if ENV["HOMEBREW_SORBET_RUNTIME"]
|
||||
# This is only supported under the brew environment.
|
||||
Homebrew.install_bundler_gems!(groups: ["sorbet"])
|
||||
require "sorbet-runtime"
|
||||
else
|
||||
require "standalone/sorbet"
|
||||
end
|
@ -98,7 +98,6 @@ $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/simplecov-0.21.2/lib"
|
||||
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/simplecov-cobertura-2.1.0/lib"
|
||||
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sorbet-static-0.5.10160-universal-darwin-15/lib"
|
||||
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sorbet-0.5.10160/lib"
|
||||
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sorbet-runtime-stub-0.2.0/lib"
|
||||
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sorbet-static-and-runtime-0.5.10160/lib"
|
||||
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/thor-1.2.1/lib"
|
||||
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/spoom-1.1.11/lib"
|
||||
|
116
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/sorbet-runtime.rb
vendored
Normal file
116
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/sorbet-runtime.rb
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
# This file is hand-crafted to encode the dependencies. They load the whole type
|
||||
# system since there is such a high chance of it being used, using an autoloader
|
||||
# wouldn't buy us any startup time saving.
|
||||
|
||||
# Namespaces without any implementation
|
||||
module T; end
|
||||
module T::Helpers; end
|
||||
module T::Private; end
|
||||
module T::Private::Abstract; end
|
||||
module T::Private::Types; end
|
||||
|
||||
# Each section is a group that I believe need a fixed ordering. There is also
|
||||
# an ordering between groups.
|
||||
|
||||
# These are pre-reqs for almost everything in here.
|
||||
require_relative 'types/configuration'
|
||||
require_relative 'types/_types'
|
||||
require_relative 'types/private/decl_state'
|
||||
require_relative 'types/private/class_utils'
|
||||
require_relative 'types/private/runtime_levels'
|
||||
require_relative 'types/private/methods/_methods'
|
||||
require_relative 'types/sig'
|
||||
require_relative 'types/helpers'
|
||||
require_relative 'types/private/final'
|
||||
require_relative 'types/private/sealed'
|
||||
|
||||
# The types themselves. First base classes
|
||||
require_relative 'types/types/base'
|
||||
require_relative 'types/types/typed_enumerable'
|
||||
# Everything else
|
||||
require_relative 'types/types/class_of'
|
||||
require_relative 'types/types/enum'
|
||||
require_relative 'types/types/fixed_array'
|
||||
require_relative 'types/types/fixed_hash'
|
||||
require_relative 'types/types/intersection'
|
||||
require_relative 'types/types/noreturn'
|
||||
require_relative 'types/types/proc'
|
||||
require_relative 'types/types/attached_class'
|
||||
require_relative 'types/types/self_type'
|
||||
require_relative 'types/types/simple'
|
||||
require_relative 'types/types/t_enum'
|
||||
require_relative 'types/types/type_parameter'
|
||||
require_relative 'types/types/typed_array'
|
||||
require_relative 'types/types/typed_enumerator'
|
||||
require_relative 'types/types/typed_enumerator_lazy'
|
||||
require_relative 'types/types/typed_hash'
|
||||
require_relative 'types/types/typed_range'
|
||||
require_relative 'types/types/typed_set'
|
||||
require_relative 'types/types/union'
|
||||
require_relative 'types/types/untyped'
|
||||
require_relative 'types/private/types/not_typed'
|
||||
require_relative 'types/private/types/void'
|
||||
require_relative 'types/private/types/string_holder'
|
||||
require_relative 'types/private/types/type_alias'
|
||||
|
||||
require_relative 'types/types/type_variable'
|
||||
require_relative 'types/types/type_member'
|
||||
require_relative 'types/types/type_template'
|
||||
|
||||
# Call validation
|
||||
require_relative 'types/private/methods/modes'
|
||||
require_relative 'types/private/methods/call_validation'
|
||||
|
||||
# Signature validation
|
||||
require_relative 'types/private/methods/signature_validation'
|
||||
require_relative 'types/abstract_utils'
|
||||
require_relative 'types/private/abstract/validate'
|
||||
|
||||
# Catch all. Sort of built by `cd extn; find types -type f | grep -v test | sort`
|
||||
require_relative 'types/generic'
|
||||
require_relative 'types/interface_wrapper'
|
||||
require_relative 'types/private/abstract/declare'
|
||||
require_relative 'types/private/abstract/hooks'
|
||||
require_relative 'types/private/casts'
|
||||
require_relative 'types/private/methods/decl_builder'
|
||||
require_relative 'types/private/methods/signature'
|
||||
require_relative 'types/private/retry'
|
||||
require_relative 'types/utils'
|
||||
require_relative 'types/boolean'
|
||||
|
||||
# Props dependencies
|
||||
require_relative 'types/private/abstract/data'
|
||||
require_relative 'types/private/mixins/mixins'
|
||||
require_relative 'types/props/_props'
|
||||
require_relative 'types/props/custom_type'
|
||||
require_relative 'types/props/decorator'
|
||||
require_relative 'types/props/errors'
|
||||
require_relative 'types/props/plugin'
|
||||
require_relative 'types/props/utils'
|
||||
require_relative 'types/enum'
|
||||
# Props that run sigs statically so have to be after all the others :(
|
||||
require_relative 'types/props/private/setter_factory'
|
||||
require_relative 'types/props/private/apply_default'
|
||||
require_relative 'types/props/has_lazily_specialized_methods'
|
||||
require_relative 'types/props/optional'
|
||||
require_relative 'types/props/weak_constructor'
|
||||
require_relative 'types/props/constructor'
|
||||
require_relative 'types/props/pretty_printable'
|
||||
require_relative 'types/props/private/serde_transform'
|
||||
require_relative 'types/props/private/deserializer_generator'
|
||||
require_relative 'types/props/private/serializer_generator'
|
||||
require_relative 'types/props/serializable'
|
||||
require_relative 'types/props/type_validation'
|
||||
require_relative 'types/props/private/parser'
|
||||
require_relative 'types/props/generated_code_validation'
|
||||
|
||||
require_relative 'types/struct'
|
||||
require_relative 'types/non_forcing_constants'
|
||||
|
||||
require_relative 'types/compatibility_patches'
|
||||
|
||||
# Sorbet Compiler support module
|
||||
require_relative 'types/private/compiler'
|
316
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/_types.rb
vendored
Normal file
316
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/_types.rb
vendored
Normal file
@ -0,0 +1,316 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
# This is where we define the shortcuts, so we can't use them here
|
||||
|
||||
# _____
|
||||
# |_ _| _ _ __ ___ ___
|
||||
# | || | | | '_ \ / _ \/ __|
|
||||
# | || |_| | |_) | __/\__ \
|
||||
# |_| \__, | .__/ \___||___/
|
||||
# |___/|_|
|
||||
#
|
||||
# Docs at https://sorbet.org/docs/sigs
|
||||
#
|
||||
# Types that you can pass to `sig`:
|
||||
#
|
||||
# - a Ruby class
|
||||
#
|
||||
# - [<Type>, <Type>, ...] -- to specify a "tuple"; a fixed-size array with known types for each member
|
||||
#
|
||||
# - {key: <Type>, key2: <Type>, ...} -- to speicfy a "shape"; a fixed-size hash
|
||||
# with known keys and type values
|
||||
#
|
||||
# - Any of the `T.foo` methods below
|
||||
|
||||
module T
|
||||
# T.any(<Type>, <Type>, ...) -- matches any of the types listed
|
||||
def self.any(type_a, type_b, *types)
|
||||
type_a = T::Utils.coerce(type_a)
|
||||
type_b = T::Utils.coerce(type_b)
|
||||
types = types.map {|t| T::Utils.coerce(t)} if !types.empty?
|
||||
T::Types::Union::Private::Pool.union_of_types(type_a, type_b, types)
|
||||
end
|
||||
|
||||
# Shorthand for T.any(type, NilClass)
|
||||
def self.nilable(type)
|
||||
T::Types::Union::Private::Pool.union_of_types(T::Utils.coerce(type), T::Utils::Nilable::NIL_TYPE)
|
||||
end
|
||||
|
||||
# Matches any object. In the static checker, T.untyped allows any
|
||||
# method calls or operations.
|
||||
def self.untyped
|
||||
T::Types::Untyped::Private::INSTANCE
|
||||
end
|
||||
|
||||
# Indicates a function never returns (e.g. "Kernel#raise")
|
||||
def self.noreturn
|
||||
T::Types::NoReturn::Private::INSTANCE
|
||||
end
|
||||
|
||||
# T.all(<Type>, <Type>, ...) -- matches an object that has all of the types listed
|
||||
def self.all(type_a, type_b, *types)
|
||||
T::Types::Intersection.new([type_a, type_b] + types)
|
||||
end
|
||||
|
||||
# Matches any of the listed values
|
||||
# @deprecated Use T::Enum instead.
|
||||
def self.deprecated_enum(values)
|
||||
T::Types::Enum.new(values)
|
||||
end
|
||||
|
||||
# Creates a proc type
|
||||
def self.proc
|
||||
T::Private::Methods.start_proc
|
||||
end
|
||||
|
||||
# Matches `self`:
|
||||
def self.self_type
|
||||
T::Types::SelfType::Private::INSTANCE
|
||||
end
|
||||
|
||||
# Matches the instance type in a singleton-class context
|
||||
def self.attached_class
|
||||
T::Types::AttachedClassType::Private::INSTANCE
|
||||
end
|
||||
|
||||
# Matches any class that subclasses or includes the provided class
|
||||
# or module
|
||||
def self.class_of(klass)
|
||||
T::Types::ClassOf.new(klass)
|
||||
end
|
||||
|
||||
## END OF THE METHODS TO PASS TO `sig`.
|
||||
|
||||
# Constructs a type alias. Used to create a short name for a larger type. In Ruby this returns a
|
||||
# wrapper that contains a proc that is evaluated to get the underlying type. This syntax however
|
||||
# is needed for support by the static checker.
|
||||
#
|
||||
# @example
|
||||
# NilableString = T.type_alias {T.nilable(String)}
|
||||
#
|
||||
# sig {params(arg: NilableString, default: String).returns(String)}
|
||||
# def or_else(arg, default)
|
||||
# arg || default
|
||||
# end
|
||||
#
|
||||
# The name of the type alias is not preserved; Error messages will
|
||||
# be printed with reference to the underlying type.
|
||||
#
|
||||
# TODO Remove `type` parameter. This was left in to make life easier while migrating.
|
||||
def self.type_alias(type=nil, &blk)
|
||||
if blk
|
||||
T::Private::Types::TypeAlias.new(blk)
|
||||
else
|
||||
T::Utils.coerce(type)
|
||||
end
|
||||
end
|
||||
|
||||
# References a type parameter which was previously defined with
|
||||
# `type_parameters`.
|
||||
#
|
||||
# This is used for generic methods.
|
||||
#
|
||||
# @example
|
||||
# sig
|
||||
# .type_parameters(:U)
|
||||
# .params(
|
||||
# blk: T.proc.params(arg0: Elem).returns(T.type_parameter(:U)),
|
||||
# )
|
||||
# .returns(T::Array[T.type_parameter(:U)])
|
||||
# def map(&blk); end
|
||||
def self.type_parameter(name)
|
||||
T::Types::TypeParameter.new(name)
|
||||
end
|
||||
|
||||
# Tells the typechecker that `value` is of type `type`. Use this to get additional checking after
|
||||
# an expression that the typechecker is unable to analyze. If `checked` is true, raises an
|
||||
# exception at runtime if the value doesn't match the type.
|
||||
#
|
||||
# Compared to `T.let`, `T.cast` is _trusted_ by static system.
|
||||
def self.cast(value, type, checked: true)
|
||||
return value unless checked
|
||||
|
||||
Private::Casts.cast(value, type, cast_method: "T.cast")
|
||||
end
|
||||
|
||||
# Tells the typechecker to declare a variable of type `type`. Use
|
||||
# like:
|
||||
#
|
||||
# seconds = T.let(0.0, Float)
|
||||
#
|
||||
# Compared to `T.cast`, `T.let` is _checked_ by static system.
|
||||
#
|
||||
# If `checked` is true, raises an exception at runtime if the value
|
||||
# doesn't match the type.
|
||||
def self.let(value, type, checked: true)
|
||||
return value unless checked
|
||||
|
||||
Private::Casts.cast(value, type, cast_method: "T.let")
|
||||
end
|
||||
|
||||
# Tells the type checker to treat `self` in the current block as `type`.
|
||||
# Useful for blocks that are captured and executed later with instance_exec.
|
||||
# Use like:
|
||||
#
|
||||
# seconds = lambda do
|
||||
# T.bind(self, NewBinding)
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# `T.bind` behaves like `T.cast` in that it is assumed to be true statically.
|
||||
#
|
||||
# If `checked` is true, raises an exception at runtime if the value
|
||||
# doesn't match the type (this is the default).
|
||||
def self.bind(value, type, checked: true)
|
||||
return value unless checked
|
||||
|
||||
Private::Casts.cast(value, type, cast_method: "T.bind")
|
||||
end
|
||||
|
||||
# Tells the typechecker to ensure that `value` is of type `type` (if not, the typechecker will
|
||||
# fail). Use this for debugging typechecking errors, or to ensure that type information is
|
||||
# statically known and being checked appropriately. If `checked` is true, raises an exception at
|
||||
# runtime if the value doesn't match the type.
|
||||
def self.assert_type!(value, type, checked: true)
|
||||
return value unless checked
|
||||
|
||||
Private::Casts.cast(value, type, cast_method: "T.assert_type!")
|
||||
end
|
||||
|
||||
# For the static type checker, strips all type information from a value
|
||||
# and returns the same value, but statically-typed as `T.untyped`.
|
||||
# Can be used to tell the static checker to "trust you" by discarding type information
|
||||
# you know to be incorrect. Use with care!
|
||||
# (This has no effect at runtime.)
|
||||
#
|
||||
# We can't actually write this sig because we ourselves are inside
|
||||
# the `T::` module and doing this would create a bootstrapping
|
||||
# cycle. However, we also don't actually need to do so; An untyped
|
||||
# identity method works just as well here.
|
||||
#
|
||||
# `sig {params(value: T.untyped).returns(T.untyped)}`
|
||||
def self.unsafe(value)
|
||||
value
|
||||
end
|
||||
|
||||
# A convenience method to `raise` when the argument is `nil` and return it
|
||||
# otherwise.
|
||||
#
|
||||
# Intended to be used as:
|
||||
#
|
||||
# needs_foo(T.must(maybe_gives_foo))
|
||||
#
|
||||
# Equivalent to:
|
||||
#
|
||||
# foo = maybe_gives_foo
|
||||
# raise "nil" if foo.nil?
|
||||
# needs_foo(foo)
|
||||
#
|
||||
# Intended to be used to promise sorbet that a given nilable value happens
|
||||
# to contain a non-nil value at this point.
|
||||
#
|
||||
# `sig {params(arg: T.nilable(A)).returns(A)}`
|
||||
def self.must(arg)
|
||||
return arg if arg
|
||||
return arg if arg == false
|
||||
|
||||
begin
|
||||
raise TypeError.new("Passed `nil` into T.must")
|
||||
rescue TypeError => e # raise into rescue to ensure e.backtrace is populated
|
||||
T::Configuration.inline_type_error_handler(e, {kind: 'T.must', value: arg, type: nil})
|
||||
end
|
||||
end
|
||||
|
||||
# A way to ask Sorbet to show what type it thinks an expression has.
|
||||
# This can be useful for debugging and checking assumptions.
|
||||
# In the runtime, merely returns the value passed in.
|
||||
def self.reveal_type(value)
|
||||
value
|
||||
end
|
||||
|
||||
# A way to ask Sorbet to prove that a certain branch of control flow never
|
||||
# happens. Commonly used to assert that a case or if statement exhausts all
|
||||
# possible cases.
|
||||
def self.absurd(value)
|
||||
msg = "Control flow reached T.absurd."
|
||||
|
||||
case value
|
||||
when Kernel
|
||||
msg += " Got value: #{value}"
|
||||
end
|
||||
|
||||
begin
|
||||
raise TypeError.new(msg)
|
||||
rescue TypeError => e # raise into rescue to ensure e.backtrace is populated
|
||||
T::Configuration.inline_type_error_handler(e, {kind: 'T.absurd', value: value, type: nil})
|
||||
end
|
||||
end
|
||||
|
||||
### Generic classes ###
|
||||
|
||||
module Array
|
||||
def self.[](type)
|
||||
if type.is_a?(T::Types::Untyped)
|
||||
T::Types::TypedArray::Untyped.new
|
||||
else
|
||||
T::Types::TypedArray.new(type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Hash
|
||||
def self.[](keys, values)
|
||||
if keys.is_a?(T::Types::Untyped) && values.is_a?(T::Types::Untyped)
|
||||
T::Types::TypedHash::Untyped.new
|
||||
else
|
||||
T::Types::TypedHash.new(keys: keys, values: values)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Enumerable
|
||||
def self.[](type)
|
||||
if type.is_a?(T::Types::Untyped)
|
||||
T::Types::TypedEnumerable::Untyped.new
|
||||
else
|
||||
T::Types::TypedEnumerable.new(type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Enumerator
|
||||
def self.[](type)
|
||||
if type.is_a?(T::Types::Untyped)
|
||||
T::Types::TypedEnumerator::Untyped.new
|
||||
else
|
||||
T::Types::TypedEnumerator.new(type)
|
||||
end
|
||||
end
|
||||
|
||||
module Lazy
|
||||
def self.[](type)
|
||||
if type.is_a?(T::Types::Untyped)
|
||||
T::Types::TypedEnumeratorLazy::Untyped.new
|
||||
else
|
||||
T::Types::TypedEnumeratorLazy.new(type)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Range
|
||||
def self.[](type)
|
||||
T::Types::TypedRange.new(type)
|
||||
end
|
||||
end
|
||||
|
||||
module Set
|
||||
def self.[](type)
|
||||
if type.is_a?(T::Types::Untyped)
|
||||
T::Types::TypedSet::Untyped.new
|
||||
else
|
||||
T::Types::TypedSet.new(type)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::AbstractUtils
|
||||
Methods = T::Private::Methods
|
||||
|
||||
# Returns whether a module is declared as abstract. After the module is finished being declared,
|
||||
# this is equivalent to whether it has any abstract methods that haven't been implemented
|
||||
# (because we validate that and raise an error otherwise).
|
||||
#
|
||||
# Note that checking `mod.is_a?(Abstract::Hooks)` is not a safe substitute for this method; when
|
||||
# a class extends `Abstract::Hooks`, all of its subclasses, including the eventual concrete
|
||||
# ones, will still have `Abstract::Hooks` as an ancestor.
|
||||
def self.abstract_module?(mod)
|
||||
!T::Private::Abstract::Data.get(mod, :abstract_type).nil?
|
||||
end
|
||||
|
||||
def self.abstract_method?(method)
|
||||
signature = Methods.signature_for_method(method)
|
||||
signature&.mode == Methods::Modes.abstract
|
||||
end
|
||||
|
||||
# Given a module, returns the set of methods declared as abstract (in itself or ancestors)
|
||||
# that have not been implemented.
|
||||
def self.abstract_methods_for(mod)
|
||||
declared_methods = declared_abstract_methods_for(mod)
|
||||
declared_methods.select do |declared_method|
|
||||
actual_method = mod.instance_method(declared_method.name)
|
||||
# Note that in the case where an abstract method is overridden by another abstract method,
|
||||
# this method will return them both. This is intentional to ensure we validate the final
|
||||
# implementation against all declarations of an abstract method (they might not all have the
|
||||
# same signature).
|
||||
abstract_method?(actual_method)
|
||||
end
|
||||
end
|
||||
|
||||
# Given a module, returns the set of methods declared as abstract (in itself or ancestors)
|
||||
# regardless of whether they have been implemented.
|
||||
def self.declared_abstract_methods_for(mod)
|
||||
methods = []
|
||||
mod.ancestors.each do |ancestor|
|
||||
ancestor_methods = ancestor.private_instance_methods(false) + ancestor.instance_methods(false)
|
||||
ancestor_methods.each do |method_name|
|
||||
method = ancestor.instance_method(method_name)
|
||||
methods << method if abstract_method?(method)
|
||||
end
|
||||
end
|
||||
methods
|
||||
end
|
||||
end
|
8
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/boolean.rb
vendored
Normal file
8
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/boolean.rb
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module T
|
||||
# T::Boolean is a type alias helper for the common `T.any(TrueClass, FalseClass)`.
|
||||
# Defined separately from _types.rb because it has a dependency on T::Types::Union.
|
||||
Boolean = T.type_alias {T.any(TrueClass, FalseClass)}
|
||||
end
|
@ -0,0 +1,93 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: ignore
|
||||
|
||||
# Work around an interaction bug with sorbet-runtime and rspec-mocks,
|
||||
# which occurs when using message expectations (*_any_instance_of,
|
||||
# expect, allow) and and_call_original.
|
||||
#
|
||||
# When a sig is defined, sorbet-runtime will replace the sigged method
|
||||
# with a wrapper that, upon first invocation, re-wraps the method with a faster
|
||||
# implementation.
|
||||
#
|
||||
# When expect_any_instance_of is used, rspec stores a reference to the first wrapper,
|
||||
# to be restored later.
|
||||
#
|
||||
# The first wrapper is invoked as part of the test and sorbet-runtime replaces
|
||||
# the method definition with the second wrapper.
|
||||
#
|
||||
# But when mocks are cleaned up, rspec restores back to the first wrapper.
|
||||
# Upon subsequent invocations, the first wrapper is called, and sorbet-runtime
|
||||
# throws a runtime error, since this is an unexpected state.
|
||||
#
|
||||
# We work around this by forcing re-wrapping before rspec stores a reference
|
||||
# to the method.
|
||||
if defined? ::RSpec::Mocks
|
||||
module T
|
||||
module CompatibilityPatches
|
||||
module RSpecCompatibility
|
||||
module RecorderExtensions
|
||||
def observe!(method_name)
|
||||
method = @klass.instance_method(method_name.to_sym)
|
||||
T::Private::Methods.maybe_run_sig_block_for_method(method)
|
||||
super(method_name)
|
||||
end
|
||||
end
|
||||
::RSpec::Mocks::AnyInstance::Recorder.prepend(RecorderExtensions) if defined?(::RSpec::Mocks::AnyInstance::Recorder)
|
||||
|
||||
module MethodDoubleExtensions
|
||||
def initialize(object, method_name, proxy)
|
||||
if ::Kernel.instance_method(:respond_to?).bind(object).call(method_name, true)
|
||||
method = ::RSpec::Support.method_handle_for(object, method_name)
|
||||
T::Private::Methods.maybe_run_sig_block_for_method(method)
|
||||
end
|
||||
super(object, method_name, proxy)
|
||||
end
|
||||
end
|
||||
::RSpec::Mocks::MethodDouble.prepend(MethodDoubleExtensions) if defined?(::RSpec::Mocks::MethodDouble)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Work around for sorbet-runtime wrapped methods.
|
||||
#
|
||||
# When a sig is defined, sorbet-runtime will replace the sigged method
|
||||
# with a wrapper. Those wrapper methods look like `foo(*args, &blk)`
|
||||
# so that wrappers can handle and pass on all the arguments supplied.
|
||||
#
|
||||
# However, that creates a problem with runtime reflection on the methods,
|
||||
# since when a sigged method is introspected, it will always return its
|
||||
# `arity` as `-1`, its `parameters` as `[[:rest, :args], [:block, :blk]]`,
|
||||
# and its `source_location` as `[<some_file_in_sorbet>, <some_line_number>]`.
|
||||
#
|
||||
# This might be a problem for some applications that rely on getting the
|
||||
# correct information from these methods.
|
||||
#
|
||||
# This compatibility module, when prepended to the `Method` class, would fix
|
||||
# the return values of `arity`, `parameters` and `source_location`.
|
||||
#
|
||||
# @example
|
||||
# require 'sorbet-runtime'
|
||||
# ::Method.prepend(T::CompatibilityPatches::MethodExtensions)
|
||||
module T
|
||||
module CompatibilityPatches
|
||||
module MethodExtensions
|
||||
def arity
|
||||
arity = super
|
||||
return arity if arity != -1 || self.is_a?(Proc)
|
||||
sig = T::Private::Methods.signature_for_method(self)
|
||||
sig ? sig.method.arity : arity
|
||||
end
|
||||
|
||||
def source_location
|
||||
sig = T::Private::Methods.signature_for_method(self)
|
||||
sig ? sig.method.source_location : super
|
||||
end
|
||||
|
||||
def parameters
|
||||
sig = T::Private::Methods.signature_for_method(self)
|
||||
sig ? sig.method.parameters : super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
591
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/configuration.rb
vendored
Normal file
591
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/configuration.rb
vendored
Normal file
@ -0,0 +1,591 @@
|
||||
# typed: true
|
||||
# frozen_string_literal: true
|
||||
|
||||
module T::Configuration
|
||||
# Cache this comparisonn to avoid two allocations all over the place.
|
||||
AT_LEAST_RUBY_2_7 = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7')
|
||||
|
||||
# Announces to Sorbet that we are currently in a test environment, so it
|
||||
# should treat any sigs which are marked `.checked(:tests)` as if they were
|
||||
# just a normal sig.
|
||||
#
|
||||
# If this method is not called, sigs marked `.checked(:tests)` will not be
|
||||
# checked. In fact, such methods won't even be wrapped--the runtime will put
|
||||
# back the original method.
|
||||
#
|
||||
# Note: Due to the way sigs are evaluated and methods are wrapped, this
|
||||
# method MUST be called before any code calls `sig`. This method raises if
|
||||
# it has been called too late.
|
||||
def self.enable_checking_for_sigs_marked_checked_tests
|
||||
T::Private::RuntimeLevels.enable_checking_in_tests
|
||||
end
|
||||
|
||||
# Announce to Sorbet that we would like the final checks to be enabled when
|
||||
# including and extending modules. Iff this is not called, then the following
|
||||
# example will not raise an error.
|
||||
#
|
||||
# ```ruby
|
||||
# module M
|
||||
# extend T::Sig
|
||||
# sig(:final) {void}
|
||||
# def foo; end
|
||||
# end
|
||||
# class C
|
||||
# include M
|
||||
# def foo; end
|
||||
# end
|
||||
# ```
|
||||
def self.enable_final_checks_on_hooks
|
||||
T::Private::Methods.set_final_checks_on_hooks(true)
|
||||
end
|
||||
|
||||
# Undo the effects of a previous call to
|
||||
# `enable_final_checks_on_hooks`.
|
||||
def self.reset_final_checks_on_hooks
|
||||
T::Private::Methods.set_final_checks_on_hooks(false)
|
||||
end
|
||||
|
||||
@include_value_in_type_errors = true
|
||||
# Whether to include values in TypeError messages.
|
||||
#
|
||||
# Including values is useful for debugging, but can potentially leak
|
||||
# sensitive information to logs.
|
||||
#
|
||||
# @return [T::Boolean]
|
||||
def self.include_value_in_type_errors?
|
||||
@include_value_in_type_errors
|
||||
end
|
||||
|
||||
# Configure if type errors excludes the value of the problematic type.
|
||||
#
|
||||
# The default is to include values in type errors:
|
||||
# TypeError: Expected type Integer, got String with value "foo"
|
||||
#
|
||||
# When values are excluded from type errors:
|
||||
# TypeError: Expected type Integer, got String
|
||||
def self.exclude_value_in_type_errors
|
||||
@include_value_in_type_errors = false
|
||||
end
|
||||
|
||||
# Opposite of exclude_value_in_type_errors.
|
||||
# (Including values in type errors is the default)
|
||||
def self.include_value_in_type_errors
|
||||
@include_value_in_type_errors = true
|
||||
end
|
||||
|
||||
# Whether VM-defined prop serialization/deserialization routines can be enabled.
|
||||
#
|
||||
# @return [T::Boolean]
|
||||
def self.can_enable_vm_prop_serde?
|
||||
T::Props::Private::DeserializerGenerator.respond_to?(:generate2)
|
||||
end
|
||||
|
||||
@use_vm_prop_serde = false
|
||||
# Whether to use VM-defined prop serialization/deserialization routines.
|
||||
#
|
||||
# The default is to use runtime codegen inside sorbet-runtime itself.
|
||||
#
|
||||
# @return [T::Boolean]
|
||||
def self.use_vm_prop_serde?
|
||||
@use_vm_prop_serde || false
|
||||
end
|
||||
|
||||
# Enable using VM-defined prop serialization/deserialization routines.
|
||||
#
|
||||
# This method is likely to break things outside of Stripe's systems.
|
||||
def self.enable_vm_prop_serde
|
||||
if !can_enable_vm_prop_serde?
|
||||
hard_assert_handler('Ruby VM is not setup to use VM-defined prop serde')
|
||||
end
|
||||
@use_vm_prop_serde = true
|
||||
end
|
||||
|
||||
# Disable using VM-defined prop serialization/deserialization routines.
|
||||
def self.disable_vm_prop_serde
|
||||
@use_vm_prop_serde = false
|
||||
end
|
||||
|
||||
# Configure the default checked level for a sig with no explicit `.checked`
|
||||
# builder. When unset, the default checked level is `:always`.
|
||||
#
|
||||
# Note: setting this option is potentially dangerous! Sorbet can't check all
|
||||
# code statically. The runtime checks complement the checks that Sorbet does
|
||||
# statically, so that methods don't have to guard themselves from being
|
||||
# called incorrectly by untyped code.
|
||||
#
|
||||
# @param [:never, :compiled, :tests, :always] default_checked_level
|
||||
def self.default_checked_level=(default_checked_level)
|
||||
T::Private::RuntimeLevels.default_checked_level = default_checked_level
|
||||
end
|
||||
|
||||
@inline_type_error_handler = nil
|
||||
# Set a handler to handle `TypeError`s raised by any in-line type assertions,
|
||||
# including `T.must`, `T.let`, `T.cast`, and `T.assert_type!`.
|
||||
#
|
||||
# By default, any `TypeError`s detected by this gem will be raised. Setting
|
||||
# inline_type_error_handler to an object that implements :call (e.g. proc or
|
||||
# lambda) allows users to customize the behavior when a `TypeError` is
|
||||
# raised on any inline type assertion.
|
||||
#
|
||||
# @param [Lambda, Proc, Object, nil] value Proc that handles the error (pass
|
||||
# nil to reset to default behavior)
|
||||
#
|
||||
# Parameters passed to value.call:
|
||||
#
|
||||
# @param [TypeError] error TypeError that was raised
|
||||
# @param [Hash] opts A hash containing contextual information on the error:
|
||||
# @option opts [String] :kind One of:
|
||||
# ['T.cast', 'T.let', 'T.bind', 'T.assert_type!', 'T.must', 'T.absurd']
|
||||
# @option opts [Object, nil] :type Expected param/return value type
|
||||
# @option opts [Object] :value Actual param/return value
|
||||
#
|
||||
# @example
|
||||
# T::Configuration.inline_type_error_handler = lambda do |error, opts|
|
||||
# puts error.message
|
||||
# end
|
||||
def self.inline_type_error_handler=(value)
|
||||
validate_lambda_given!(value)
|
||||
@inline_type_error_handler = value
|
||||
end
|
||||
|
||||
private_class_method def self.inline_type_error_handler_default(error, opts)
|
||||
raise error
|
||||
end
|
||||
|
||||
def self.inline_type_error_handler(error, opts={})
|
||||
if @inline_type_error_handler
|
||||
# Backwards compatibility before `inline_type_error_handler` took a second arg
|
||||
if @inline_type_error_handler.arity == 1
|
||||
@inline_type_error_handler.call(error)
|
||||
else
|
||||
@inline_type_error_handler.call(error, opts)
|
||||
end
|
||||
else
|
||||
inline_type_error_handler_default(error, opts)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
@sig_builder_error_handler = nil
|
||||
# Set a handler to handle errors that occur when the builder methods in the
|
||||
# body of a sig are executed. The sig builder methods are inside a proc so
|
||||
# that they can be lazily evaluated the first time the method being sig'd is
|
||||
# called.
|
||||
#
|
||||
# By default, improper use of the builder methods within the body of a sig
|
||||
# cause an ArgumentError to be raised. Setting sig_builder_error_handler to an
|
||||
# object that implements :call (e.g. proc or lambda) allows users to
|
||||
# customize the behavior when a sig can't be built for some reason.
|
||||
#
|
||||
# @param [Lambda, Proc, Object, nil] value Proc that handles the error (pass
|
||||
# nil to reset to default behavior)
|
||||
#
|
||||
# Parameters passed to value.call:
|
||||
#
|
||||
# @param [StandardError] error The error that was raised
|
||||
# @param [Thread::Backtrace::Location] location Location of the error
|
||||
#
|
||||
# @example
|
||||
# T::Configuration.sig_builder_error_handler = lambda do |error, location|
|
||||
# puts error.message
|
||||
# end
|
||||
def self.sig_builder_error_handler=(value)
|
||||
validate_lambda_given!(value)
|
||||
@sig_builder_error_handler = value
|
||||
end
|
||||
|
||||
private_class_method def self.sig_builder_error_handler_default(error, location)
|
||||
raise ArgumentError.new("#{location.path}:#{location.lineno}: Error interpreting `sig`:\n #{error.message}\n\n")
|
||||
end
|
||||
|
||||
def self.sig_builder_error_handler(error, location)
|
||||
if @sig_builder_error_handler
|
||||
@sig_builder_error_handler.call(error, location)
|
||||
else
|
||||
sig_builder_error_handler_default(error, location)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
@sig_validation_error_handler = nil
|
||||
# Set a handler to handle sig validation errors.
|
||||
#
|
||||
# Sig validation errors include things like abstract checks, override checks,
|
||||
# and type compatibility of arguments. They happen after a sig has been
|
||||
# successfully built, but the built sig is incompatible with other sigs in
|
||||
# some way.
|
||||
#
|
||||
# By default, sig validation errors cause an exception to be raised.
|
||||
# Setting sig_validation_error_handler to an object that implements :call
|
||||
# (e.g. proc or lambda) allows users to customize the behavior when a method
|
||||
# signature's build fails.
|
||||
#
|
||||
# @param [Lambda, Proc, Object, nil] value Proc that handles the error (pass
|
||||
# nil to reset to default behavior)
|
||||
#
|
||||
# Parameters passed to value.call:
|
||||
#
|
||||
# @param [StandardError] error The error that was raised
|
||||
# @param [Hash] opts A hash containing contextual information on the error:
|
||||
# @option opts [Method, UnboundMethod] :method Method on which the signature build failed
|
||||
# @option opts [T::Private::Methods::Declaration] :declaration Method
|
||||
# signature declaration struct
|
||||
# @option opts [T::Private::Methods::Signature, nil] :signature Signature
|
||||
# that failed (nil if sig build failed before Signature initialization)
|
||||
# @option opts [T::Private::Methods::Signature, nil] :super_signature Super
|
||||
# method's signature (nil if method is not an override or super method
|
||||
# does not have a method signature)
|
||||
#
|
||||
# @example
|
||||
# T::Configuration.sig_validation_error_handler = lambda do |error, opts|
|
||||
# puts error.message
|
||||
# end
|
||||
def self.sig_validation_error_handler=(value)
|
||||
validate_lambda_given!(value)
|
||||
@sig_validation_error_handler = value
|
||||
end
|
||||
|
||||
private_class_method def self.sig_validation_error_handler_default(error, opts)
|
||||
raise error
|
||||
end
|
||||
|
||||
def self.sig_validation_error_handler(error, opts={})
|
||||
if @sig_validation_error_handler
|
||||
@sig_validation_error_handler.call(error, opts)
|
||||
else
|
||||
sig_validation_error_handler_default(error, opts)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
@call_validation_error_handler = nil
|
||||
# Set a handler for type errors that result from calling a method.
|
||||
#
|
||||
# By default, errors from calling a method cause an exception to be raised.
|
||||
# Setting call_validation_error_handler to an object that implements :call
|
||||
# (e.g. proc or lambda) allows users to customize the behavior when a method
|
||||
# is called with invalid parameters, or returns an invalid value.
|
||||
#
|
||||
# @param [Lambda, Proc, Object, nil] value Proc that handles the error
|
||||
# report (pass nil to reset to default behavior)
|
||||
#
|
||||
# Parameters passed to value.call:
|
||||
#
|
||||
# @param [T::Private::Methods::Signature] signature Signature that failed
|
||||
# @param [Hash] opts A hash containing contextual information on the error:
|
||||
# @option opts [String] :message Error message
|
||||
# @option opts [String] :kind One of:
|
||||
# ['Parameter', 'Block parameter', 'Return value']
|
||||
# @option opts [Symbol] :name Param or block param name (nil for return
|
||||
# value)
|
||||
# @option opts [Object] :type Expected param/return value type
|
||||
# @option opts [Object] :value Actual param/return value
|
||||
# @option opts [Thread::Backtrace::Location] :location Location of the
|
||||
# caller
|
||||
#
|
||||
# @example
|
||||
# T::Configuration.call_validation_error_handler = lambda do |signature, opts|
|
||||
# puts opts[:message]
|
||||
# end
|
||||
def self.call_validation_error_handler=(value)
|
||||
validate_lambda_given!(value)
|
||||
@call_validation_error_handler = value
|
||||
end
|
||||
|
||||
private_class_method def self.call_validation_error_handler_default(signature, opts)
|
||||
raise TypeError.new(opts[:pretty_message])
|
||||
end
|
||||
|
||||
def self.call_validation_error_handler(signature, opts={})
|
||||
if @call_validation_error_handler
|
||||
@call_validation_error_handler.call(signature, opts)
|
||||
else
|
||||
call_validation_error_handler_default(signature, opts)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
@log_info_handler = nil
|
||||
# Set a handler for logging
|
||||
#
|
||||
# @param [Lambda, Proc, Object, nil] value Proc that handles the error
|
||||
# report (pass nil to reset to default behavior)
|
||||
#
|
||||
# Parameters passed to value.call:
|
||||
#
|
||||
# @param [String] str Message to be logged
|
||||
# @param [Hash] extra A hash containing additional parameters to be passed along to the logger.
|
||||
#
|
||||
# @example
|
||||
# T::Configuration.log_info_handler = lambda do |str, extra|
|
||||
# puts "#{str}, context: #{extra}"
|
||||
# end
|
||||
def self.log_info_handler=(value)
|
||||
validate_lambda_given!(value)
|
||||
@log_info_handler = value
|
||||
end
|
||||
|
||||
private_class_method def self.log_info_handler_default(str, extra)
|
||||
puts "#{str}, extra: #{extra}"
|
||||
end
|
||||
|
||||
def self.log_info_handler(str, extra)
|
||||
if @log_info_handler
|
||||
@log_info_handler.call(str, extra)
|
||||
else
|
||||
log_info_handler_default(str, extra)
|
||||
end
|
||||
end
|
||||
|
||||
@soft_assert_handler = nil
|
||||
# Set a handler for soft assertions
|
||||
#
|
||||
# These generally shouldn't stop execution of the program, but rather inform
|
||||
# some party of the assertion to action on later.
|
||||
#
|
||||
# @param [Lambda, Proc, Object, nil] value Proc that handles the error
|
||||
# report (pass nil to reset to default behavior)
|
||||
#
|
||||
# Parameters passed to value.call:
|
||||
#
|
||||
# @param [String] str Assertion message
|
||||
# @param [Hash] extra A hash containing additional parameters to be passed along to the handler.
|
||||
#
|
||||
# @example
|
||||
# T::Configuration.soft_assert_handler = lambda do |str, extra|
|
||||
# puts "#{str}, context: #{extra}"
|
||||
# end
|
||||
def self.soft_assert_handler=(value)
|
||||
validate_lambda_given!(value)
|
||||
@soft_assert_handler = value
|
||||
end
|
||||
|
||||
private_class_method def self.soft_assert_handler_default(str, extra)
|
||||
puts "#{str}, extra: #{extra}"
|
||||
end
|
||||
|
||||
def self.soft_assert_handler(str, extra)
|
||||
if @soft_assert_handler
|
||||
@soft_assert_handler.call(str, extra)
|
||||
else
|
||||
soft_assert_handler_default(str, extra)
|
||||
end
|
||||
end
|
||||
|
||||
@hard_assert_handler = nil
|
||||
# Set a handler for hard assertions
|
||||
#
|
||||
# These generally should stop execution of the program, and optionally inform
|
||||
# some party of the assertion.
|
||||
#
|
||||
# @param [Lambda, Proc, Object, nil] value Proc that handles the error
|
||||
# report (pass nil to reset to default behavior)
|
||||
#
|
||||
# Parameters passed to value.call:
|
||||
#
|
||||
# @param [String] str Assertion message
|
||||
# @param [Hash] extra A hash containing additional parameters to be passed along to the handler.
|
||||
#
|
||||
# @example
|
||||
# T::Configuration.hard_assert_handler = lambda do |str, extra|
|
||||
# raise "#{str}, context: #{extra}"
|
||||
# end
|
||||
def self.hard_assert_handler=(value)
|
||||
validate_lambda_given!(value)
|
||||
@hard_assert_handler = value
|
||||
end
|
||||
|
||||
private_class_method def self.hard_assert_handler_default(str, _)
|
||||
raise str
|
||||
end
|
||||
|
||||
def self.hard_assert_handler(str, extra={})
|
||||
if @hard_assert_handler
|
||||
@hard_assert_handler.call(str, extra)
|
||||
else
|
||||
hard_assert_handler_default(str, extra)
|
||||
end
|
||||
end
|
||||
|
||||
@scalar_types = nil
|
||||
# Set a list of class strings that are to be considered scalar.
|
||||
# (pass nil to reset to default behavior)
|
||||
#
|
||||
# @param [String] values Class name.
|
||||
#
|
||||
# @example
|
||||
# T::Configuration.scalar_types = ["NilClass", "TrueClass", "FalseClass", ...]
|
||||
def self.scalar_types=(values)
|
||||
if values.nil?
|
||||
@scalar_types = values
|
||||
else
|
||||
bad_values = values.reject {|v| v.class == String}
|
||||
unless bad_values.empty?
|
||||
raise ArgumentError.new("Provided values must all be class name strings.")
|
||||
end
|
||||
|
||||
@scalar_types = values.each_with_object({}) {|x, acc| acc[x] = true}.freeze
|
||||
end
|
||||
end
|
||||
|
||||
@default_scalar_types = {
|
||||
"NilClass" => true,
|
||||
"TrueClass" => true,
|
||||
"FalseClass" => true,
|
||||
"Integer" => true,
|
||||
"Float" => true,
|
||||
"String" => true,
|
||||
"Symbol" => true,
|
||||
"Time" => true,
|
||||
"T::Enum" => true,
|
||||
}.freeze
|
||||
|
||||
def self.scalar_types
|
||||
@scalar_types || @default_scalar_types
|
||||
end
|
||||
|
||||
# Guard against overrides of `name` or `to_s`
|
||||
MODULE_NAME = Module.instance_method(:name)
|
||||
private_constant :MODULE_NAME
|
||||
|
||||
@default_module_name_mangler = if T::Configuration::AT_LEAST_RUBY_2_7
|
||||
->(type) {MODULE_NAME.bind_call(type)}
|
||||
else
|
||||
->(type) {MODULE_NAME.bind(type).call}
|
||||
end
|
||||
|
||||
@module_name_mangler = nil
|
||||
|
||||
def self.module_name_mangler
|
||||
@module_name_mangler || @default_module_name_mangler
|
||||
end
|
||||
|
||||
# Set to override the default behavior for converting types
|
||||
# to names in generated code. Used by the runtime implementation
|
||||
# associated with `--stripe-packages` mode.
|
||||
#
|
||||
# @param [Lambda, Proc, nil] handler Proc that converts a type (Class/Module)
|
||||
# to a String (pass nil to reset to default behavior)
|
||||
def self.module_name_mangler=(handler)
|
||||
@module_name_mangler = handler
|
||||
end
|
||||
|
||||
@sensitivity_and_pii_handler = nil
|
||||
# Set to a PII handler function. This will be called with the `sensitivity:`
|
||||
# annotations on things that use `T::Props` and can modify them ahead-of-time.
|
||||
#
|
||||
# @param [Lambda, Proc, nil] handler Proc that takes a hash mapping symbols to the
|
||||
# prop values. Pass nil to avoid changing `sensitivity:` annotations.
|
||||
def self.normalize_sensitivity_and_pii_handler=(handler)
|
||||
@sensitivity_and_pii_handler = handler
|
||||
end
|
||||
|
||||
def self.normalize_sensitivity_and_pii_handler
|
||||
@sensitivity_and_pii_handler
|
||||
end
|
||||
|
||||
@redaction_handler = nil
|
||||
# Set to a redaction handling function. This will be called when the
|
||||
# `_redacted` version of a prop reader is used. By default this is set to
|
||||
# `nil` and will raise an exception when the redacted version of a prop is
|
||||
# accessed.
|
||||
#
|
||||
# @param [Lambda, Proc, nil] handler Proc that converts a value into its
|
||||
# redacted version according to the spec passed as the second argument.
|
||||
def self.redaction_handler=(handler)
|
||||
@redaction_handler = handler
|
||||
end
|
||||
|
||||
def self.redaction_handler
|
||||
@redaction_handler
|
||||
end
|
||||
|
||||
@class_owner_finder = nil
|
||||
# Set to a function which can get the 'owner' of a class. This is
|
||||
# used in reporting deserialization errors
|
||||
#
|
||||
# @param [Lambda, Proc, nil] handler Proc that takes a class and
|
||||
# produces its owner, or `nil` if it does not have one.
|
||||
def self.class_owner_finder=(handler)
|
||||
@class_owner_finder = handler
|
||||
end
|
||||
|
||||
def self.class_owner_finder
|
||||
@class_owner_finder
|
||||
end
|
||||
|
||||
# Temporarily disable ruby warnings while executing the given block. This is
|
||||
# useful when doing something that would normally cause a warning to be
|
||||
# emitted in Ruby verbose mode ($VERBOSE = true).
|
||||
#
|
||||
# @yield
|
||||
#
|
||||
def self.without_ruby_warnings
|
||||
if $VERBOSE
|
||||
begin
|
||||
original_verbose = $VERBOSE
|
||||
$VERBOSE = false
|
||||
yield
|
||||
ensure
|
||||
$VERBOSE = original_verbose
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
@legacy_t_enum_migration_mode = false
|
||||
def self.enable_legacy_t_enum_migration_mode
|
||||
@legacy_t_enum_migration_mode = true
|
||||
end
|
||||
def self.disable_legacy_t_enum_migration_mode
|
||||
@legacy_t_enum_migration_mode = false
|
||||
end
|
||||
def self.legacy_t_enum_migration_mode?
|
||||
@legacy_t_enum_migration_mode || false
|
||||
end
|
||||
|
||||
@prop_freeze_handler = ->(instance, prop_name) {}
|
||||
|
||||
def self.prop_freeze_handler=(handler)
|
||||
@prop_freeze_handler = handler
|
||||
end
|
||||
|
||||
def self.prop_freeze_handler
|
||||
@prop_freeze_handler
|
||||
end
|
||||
|
||||
@sealed_violation_whitelist = nil
|
||||
# @param [Array] sealed_violation_whitelist An array of Regexp to validate
|
||||
# whether inheriting /including a sealed module outside the defining module
|
||||
# should be allowed. Useful to whitelist benign violations, like shim files
|
||||
# generated for an autoloader.
|
||||
def self.sealed_violation_whitelist=(sealed_violation_whitelist)
|
||||
if !@sealed_violation_whitelist.nil?
|
||||
raise ArgumentError.new("Cannot overwrite sealed_violation_whitelist after setting it")
|
||||
end
|
||||
|
||||
case sealed_violation_whitelist
|
||||
when Array
|
||||
sealed_violation_whitelist.each do |x|
|
||||
case x
|
||||
when Regexp then nil
|
||||
else raise TypeError.new("sealed_violation_whitelist accepts an Array of Regexp")
|
||||
end
|
||||
end
|
||||
else
|
||||
raise TypeError.new("sealed_violation_whitelist= accepts an Array of Regexp")
|
||||
end
|
||||
|
||||
@sealed_violation_whitelist = sealed_violation_whitelist
|
||||
end
|
||||
def self.sealed_violation_whitelist
|
||||
@sealed_violation_whitelist
|
||||
end
|
||||
|
||||
private_class_method def self.validate_lambda_given!(value)
|
||||
if !value.nil? && !value.respond_to?(:call)
|
||||
raise ArgumentError.new("Provided value must respond to :call")
|
||||
end
|
||||
end
|
||||
end
|
377
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/enum.rb
vendored
Normal file
377
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/enum.rb
vendored
Normal file
@ -0,0 +1,377 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
# Enumerations allow for type-safe declarations of a fixed set of values.
|
||||
#
|
||||
# Every value is a singleton instance of the class (i.e. `Suit::SPADE.is_a?(Suit) == true`).
|
||||
#
|
||||
# Each value has a corresponding serialized value. By default this is the constant's name converted
|
||||
# to lowercase (e.g. `Suit::Club.serialize == 'club'`); however a custom value may be passed to the
|
||||
# constructor. Enum will `freeze` the serialized value.
|
||||
#
|
||||
# @example Declaring an Enum:
|
||||
# class Suit < T::Enum
|
||||
# enums do
|
||||
# CLUB = new
|
||||
# SPADE = new
|
||||
# DIAMOND = new
|
||||
# HEART = new
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# @example Custom serialization value:
|
||||
# class Status < T::Enum
|
||||
# enums do
|
||||
# READY = new('rdy')
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# @example Accessing values:
|
||||
# Suit::SPADE
|
||||
#
|
||||
# @example Converting from serialized value to enum instance:
|
||||
# Suit.deserialize('club') == Suit::CLUB
|
||||
#
|
||||
# @example Using enums in type signatures:
|
||||
# sig {params(suit: Suit).returns(Boolean)}
|
||||
# def is_red?(suit); ...; end
|
||||
#
|
||||
# WARNING: Enum instances are singletons that are shared among all their users. Their internals
|
||||
# should be kept immutable to avoid unpredictable action at a distance.
|
||||
class T::Enum
|
||||
extend T::Sig
|
||||
extend T::Props::CustomType
|
||||
|
||||
# TODO(jez) Might want to restrict this, or make subclasses provide this type
|
||||
SerializedVal = T.type_alias {T.untyped}
|
||||
private_constant :SerializedVal
|
||||
|
||||
### Enum class methods ###
|
||||
sig {returns(T::Array[T.attached_class])}
|
||||
def self.values
|
||||
if @values.nil?
|
||||
raise "Attempting to access values of #{self.class} before it has been initialized." \
|
||||
" Enums are not initialized until the 'enums do' block they are defined in has finished running."
|
||||
end
|
||||
@values
|
||||
end
|
||||
|
||||
# This exists for compatibility with the interface of `Hash` & mostly to support
|
||||
# the HashEachMethods Rubocop.
|
||||
sig {params(blk: T.nilable(T.proc.params(arg0: T.attached_class).void)).returns(T.any(T::Enumerator[T.attached_class], T::Array[T.attached_class]))}
|
||||
def self.each_value(&blk)
|
||||
if blk
|
||||
values.each(&blk)
|
||||
else
|
||||
values.each
|
||||
end
|
||||
end
|
||||
|
||||
# Convert from serialized value to enum instance
|
||||
#
|
||||
# Note: It would have been nice to make this method final before people started overriding it.
|
||||
# Note: Failed CriticalMethodsNoRuntimeTypingTest
|
||||
sig {params(serialized_val: SerializedVal).returns(T.nilable(T.attached_class)).checked(:never)}
|
||||
def self.try_deserialize(serialized_val)
|
||||
if @mapping.nil?
|
||||
raise "Attempting to access serialization map of #{self.class} before it has been initialized." \
|
||||
" Enums are not initialized until the 'enums do' block they are defined in has finished running."
|
||||
end
|
||||
@mapping[serialized_val]
|
||||
end
|
||||
|
||||
# Convert from serialized value to enum instance.
|
||||
#
|
||||
# Note: It would have been nice to make this method final before people started overriding it.
|
||||
# Note: Failed CriticalMethodsNoRuntimeTypingTest
|
||||
#
|
||||
# @return [self]
|
||||
# @raise [KeyError] if serialized value does not match any instance.
|
||||
sig {overridable.params(serialized_val: SerializedVal).returns(T.attached_class).checked(:never)}
|
||||
def self.from_serialized(serialized_val)
|
||||
res = try_deserialize(serialized_val)
|
||||
if res.nil?
|
||||
raise KeyError.new("Enum #{self} key not found: #{serialized_val.inspect}")
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
# Note: It would have been nice to make this method final before people started overriding it.
|
||||
# @return [Boolean] Does the given serialized value correspond with any of this enum's values.
|
||||
sig {overridable.params(serialized_val: SerializedVal).returns(T::Boolean).checked(:never)}
|
||||
def self.has_serialized?(serialized_val)
|
||||
if @mapping.nil?
|
||||
raise "Attempting to access serialization map of #{self.class} before it has been initialized." \
|
||||
" Enums are not initialized until the 'enums do' block they are defined in has finished running."
|
||||
end
|
||||
@mapping.include?(serialized_val)
|
||||
end
|
||||
|
||||
# Note: Failed CriticalMethodsNoRuntimeTypingTest
|
||||
sig {override.params(instance: T.nilable(T::Enum)).returns(SerializedVal).checked(:never)}
|
||||
def self.serialize(instance)
|
||||
# This is needed otherwise if a Chalk::ODM::Document with a property of the shape
|
||||
# T::Hash[T.nilable(MyEnum), Integer] and a value that looks like {nil => 0} is
|
||||
# serialized, we throw the error on L102.
|
||||
return nil if instance.nil?
|
||||
|
||||
if self == T::Enum
|
||||
raise "Cannot call T::Enum.serialize directly. You must call on a specific child class."
|
||||
end
|
||||
if instance.class != self
|
||||
raise "Cannot call #serialize on a value that is not an instance of #{self}."
|
||||
end
|
||||
instance.serialize
|
||||
end
|
||||
|
||||
# Note: Failed CriticalMethodsNoRuntimeTypingTest
|
||||
sig {override.params(mongo_value: SerializedVal).returns(T.attached_class).checked(:never)}
|
||||
def self.deserialize(mongo_value)
|
||||
if self == T::Enum
|
||||
raise "Cannot call T::Enum.deserialize directly. You must call on a specific child class."
|
||||
end
|
||||
self.from_serialized(mongo_value)
|
||||
end
|
||||
|
||||
### Enum instance methods ###
|
||||
|
||||
sig {returns(T.self_type)}
|
||||
def dup
|
||||
self
|
||||
end
|
||||
|
||||
sig {returns(T.self_type).checked(:tests)}
|
||||
def clone
|
||||
self
|
||||
end
|
||||
|
||||
# Note: Failed CriticalMethodsNoRuntimeTypingTest
|
||||
sig {returns(SerializedVal).checked(:never)}
|
||||
def serialize
|
||||
assert_bound!
|
||||
@serialized_val
|
||||
end
|
||||
|
||||
sig {params(args: T.untyped).returns(T.untyped)}
|
||||
def to_json(*args)
|
||||
serialize.to_json(*args)
|
||||
end
|
||||
|
||||
sig {params(args: T.untyped).returns(T.untyped)}
|
||||
def as_json(*args)
|
||||
serialized_val = serialize
|
||||
return serialized_val unless serialized_val.respond_to?(:as_json)
|
||||
serialized_val.as_json(*args)
|
||||
end
|
||||
|
||||
sig {returns(String)}
|
||||
def to_s
|
||||
inspect
|
||||
end
|
||||
|
||||
sig {returns(String)}
|
||||
def inspect
|
||||
"#<#{self.class.name}::#{@const_name || '__UNINITIALIZED__'}>"
|
||||
end
|
||||
|
||||
sig {params(other: BasicObject).returns(T.nilable(Integer))}
|
||||
def <=>(other)
|
||||
case other
|
||||
when self.class
|
||||
self.serialize <=> other.serialize
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# NB: Do not call this method. This exists to allow for a safe migration path in places where enum
|
||||
# values are compared directly against string values.
|
||||
#
|
||||
# Ruby's string has a weird quirk where `'my_string' == obj` calls obj.==('my_string') if obj
|
||||
# responds to the `to_str` method. It does not actually call `to_str` however.
|
||||
#
|
||||
# See https://ruby-doc.org/core-2.4.0/String.html#method-i-3D-3D
|
||||
sig {returns(String)}
|
||||
def to_str
|
||||
msg = 'Implicit conversion of Enum instances to strings is not allowed. Call #serialize instead.'
|
||||
if T::Configuration.legacy_t_enum_migration_mode?
|
||||
T::Configuration.soft_assert_handler(
|
||||
msg,
|
||||
storytime: {class: self.class.name},
|
||||
)
|
||||
serialize.to_s
|
||||
else
|
||||
raise NoMethodError.new(msg)
|
||||
end
|
||||
end
|
||||
|
||||
sig {params(other: BasicObject).returns(T::Boolean).checked(:never)}
|
||||
def ==(other)
|
||||
case other
|
||||
when String
|
||||
if T::Configuration.legacy_t_enum_migration_mode?
|
||||
comparison_assertion_failed(:==, other)
|
||||
self.serialize == other
|
||||
else
|
||||
false
|
||||
end
|
||||
else
|
||||
super(other)
|
||||
end
|
||||
end
|
||||
|
||||
sig {params(other: BasicObject).returns(T::Boolean).checked(:never)}
|
||||
def ===(other)
|
||||
case other
|
||||
when String
|
||||
if T::Configuration.legacy_t_enum_migration_mode?
|
||||
comparison_assertion_failed(:===, other)
|
||||
self.serialize == other
|
||||
else
|
||||
false
|
||||
end
|
||||
else
|
||||
super(other)
|
||||
end
|
||||
end
|
||||
|
||||
sig {params(method: Symbol, other: T.untyped).void}
|
||||
private def comparison_assertion_failed(method, other)
|
||||
T::Configuration.soft_assert_handler(
|
||||
'Enum to string comparison not allowed. Compare to the Enum instance directly instead. See go/enum-migration',
|
||||
storytime: {
|
||||
class: self.class.name,
|
||||
self: self.inspect,
|
||||
other: other,
|
||||
other_class: other.class.name,
|
||||
method: method,
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
### Private implementation ###
|
||||
|
||||
sig {params(serialized_val: SerializedVal).void}
|
||||
def initialize(serialized_val=nil)
|
||||
raise 'T::Enum is abstract' if self.class == T::Enum
|
||||
if !self.class.started_initializing?
|
||||
raise "Must instantiate all enum values of #{self.class} inside 'enums do'."
|
||||
end
|
||||
if self.class.fully_initialized?
|
||||
raise "Cannot instantiate a new enum value of #{self.class} after it has been initialized."
|
||||
end
|
||||
|
||||
serialized_val = serialized_val.frozen? ? serialized_val : serialized_val.dup.freeze
|
||||
@serialized_val = T.let(serialized_val, T.nilable(SerializedVal))
|
||||
@const_name = T.let(nil, T.nilable(Symbol))
|
||||
self.class._register_instance(self)
|
||||
end
|
||||
|
||||
sig {returns(NilClass).checked(:never)}
|
||||
private def assert_bound!
|
||||
if @const_name.nil?
|
||||
raise "Attempting to access Enum value on #{self.class} before it has been initialized." \
|
||||
" Enums are not initialized until the 'enums do' block they are defined in has finished running."
|
||||
end
|
||||
end
|
||||
|
||||
sig {params(const_name: Symbol).void}
|
||||
def _bind_name(const_name)
|
||||
@const_name = const_name
|
||||
@serialized_val = const_to_serialized_val(const_name) if @serialized_val.nil?
|
||||
freeze
|
||||
end
|
||||
|
||||
sig {params(const_name: Symbol).returns(String)}
|
||||
private def const_to_serialized_val(const_name)
|
||||
# Historical note: We convert to lowercase names because the majority of existing calls to
|
||||
# `make_accessible` were arrays of lowercase strings. Doing this conversion allowed for the
|
||||
# least amount of repetition in migrated declarations.
|
||||
-const_name.to_s.downcase.freeze
|
||||
end
|
||||
|
||||
sig {returns(T::Boolean)}
|
||||
def self.started_initializing?
|
||||
unless defined?(@started_initializing)
|
||||
@started_initializing = T.let(false, T.nilable(T::Boolean))
|
||||
end
|
||||
T.must(@started_initializing)
|
||||
end
|
||||
|
||||
sig {returns(T::Boolean)}
|
||||
def self.fully_initialized?
|
||||
unless defined?(@fully_initialized)
|
||||
@fully_initialized = T.let(false, T.nilable(T::Boolean))
|
||||
end
|
||||
T.must(@fully_initialized)
|
||||
end
|
||||
|
||||
# Maintains the order in which values are defined
|
||||
sig {params(instance: T.untyped).void}
|
||||
def self._register_instance(instance)
|
||||
@values ||= []
|
||||
@values << T.cast(instance, T.attached_class)
|
||||
end
|
||||
|
||||
# Entrypoint for allowing people to register new enum values.
|
||||
# All enum values must be defined within this block.
|
||||
sig {params(blk: T.proc.void).void}
|
||||
def self.enums(&blk)
|
||||
raise "enums cannot be defined for T::Enum" if self == T::Enum
|
||||
raise "Enum #{self} was already initialized" if fully_initialized?
|
||||
raise "Enum #{self} is still initializing" if started_initializing?
|
||||
|
||||
@started_initializing = true
|
||||
|
||||
@values = T.let(nil, T.nilable(T::Array[T.attached_class]))
|
||||
|
||||
yield
|
||||
|
||||
@mapping = T.let(nil, T.nilable(T::Hash[SerializedVal, T.attached_class]))
|
||||
@mapping = {}
|
||||
|
||||
# Freeze the Enum class and bind the constant names into each of the instances.
|
||||
self.constants(false).each do |const_name|
|
||||
instance = self.const_get(const_name, false)
|
||||
if !instance.is_a?(self)
|
||||
raise "Invalid constant #{self}::#{const_name} on enum. " \
|
||||
"All constants defined for an enum must be instances itself (e.g. `Foo = new`)."
|
||||
end
|
||||
|
||||
instance._bind_name(const_name)
|
||||
serialized = instance.serialize
|
||||
if @mapping.include?(serialized)
|
||||
raise "Enum values must have unique serializations. Value '#{serialized}' is repeated on #{self}."
|
||||
end
|
||||
@mapping[serialized] = instance
|
||||
end
|
||||
@values.freeze
|
||||
@mapping.freeze
|
||||
|
||||
orphaned_instances = T.must(@values) - @mapping.values
|
||||
if !orphaned_instances.empty?
|
||||
raise "Enum values must be assigned to constants: #{orphaned_instances.map {|v| v.instance_variable_get('@serialized_val')}}"
|
||||
end
|
||||
|
||||
@fully_initialized = true
|
||||
end
|
||||
|
||||
sig {params(child_class: Module).void}
|
||||
def self.inherited(child_class)
|
||||
super
|
||||
|
||||
raise "Inheriting from children of T::Enum is prohibited" if self != T::Enum
|
||||
end
|
||||
|
||||
# Marshal support
|
||||
sig {params(_level: Integer).returns(String)}
|
||||
def _dump(_level)
|
||||
Marshal.dump(serialize)
|
||||
end
|
||||
|
||||
sig {params(args: String).returns(T.attached_class)}
|
||||
def self._load(args)
|
||||
deserialize(Marshal.load(args)) # rubocop:disable Security/MarshalLoad
|
||||
end
|
||||
end
|
22
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/generic.rb
vendored
Normal file
22
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/generic.rb
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
# Use as a mixin with extend (`extend T::Generic`).
|
||||
module T::Generic
|
||||
include T::Helpers
|
||||
include Kernel
|
||||
|
||||
### Class/Module Helpers ###
|
||||
|
||||
def [](*types)
|
||||
self
|
||||
end
|
||||
|
||||
def type_member(variance=:invariant, &blk)
|
||||
T::Types::TypeMember.new(variance)
|
||||
end
|
||||
|
||||
def type_template(variance=:invariant, &blk)
|
||||
T::Types::TypeTemplate.new(variance)
|
||||
end
|
||||
end
|
58
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/helpers.rb
vendored
Normal file
58
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/helpers.rb
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
# Use as a mixin with extend (`extend T::Helpers`).
|
||||
# Docs at https://sorbet.org/docs/
|
||||
module T::Helpers
|
||||
extend T::Sig
|
||||
|
||||
Private = T::Private
|
||||
|
||||
### Class/Module Helpers ###
|
||||
|
||||
def abstract!
|
||||
Private::Abstract::Declare.declare_abstract(self, type: :abstract)
|
||||
end
|
||||
|
||||
def interface!
|
||||
Private::Abstract::Declare.declare_abstract(self, type: :interface)
|
||||
end
|
||||
|
||||
def final!
|
||||
Private::Final.declare(self)
|
||||
end
|
||||
|
||||
def sealed!
|
||||
Private::Sealed.declare(self, Kernel.caller(1..1)&.first&.split(':')&.first)
|
||||
end
|
||||
|
||||
# Causes a mixin to also mix in class methods from the named module.
|
||||
#
|
||||
# Nearly equivalent to
|
||||
#
|
||||
# def self.included(other)
|
||||
# other.extend(mod)
|
||||
# end
|
||||
#
|
||||
# Except that it is statically analyzed by sorbet.
|
||||
def mixes_in_class_methods(mod, *mods)
|
||||
Private::Mixins.declare_mixes_in_class_methods(self, [mod].concat(mods))
|
||||
end
|
||||
|
||||
# Specify an inclusion or inheritance requirement for `self`.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# module MyHelper
|
||||
# extend T::Helpers
|
||||
#
|
||||
# requires_ancestor { Kernel }
|
||||
# end
|
||||
#
|
||||
# class MyClass < BasicObject # error: `MyClass` must include `Kernel` (required by `MyHelper`)
|
||||
# include MyHelper
|
||||
# end
|
||||
#
|
||||
# TODO: implement the checks in sorbet-runtime.
|
||||
def requires_ancestor(&block); end
|
||||
end
|
@ -0,0 +1,158 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
# Wraps an object, exposing only the methods defined on a given class/module. The idea is that, in
|
||||
# the absence of a static type checker that would prevent you from calling non-Bar methods on a
|
||||
# variable of type Bar, we can use these wrappers as a way of enforcing it at runtime.
|
||||
#
|
||||
# Once we ship static type checking, we should get rid of this entirely.
|
||||
class T::InterfaceWrapper
|
||||
extend T::Sig
|
||||
|
||||
module Helpers
|
||||
def wrap_instance(obj)
|
||||
T::InterfaceWrapper.wrap_instance(obj, self)
|
||||
end
|
||||
|
||||
def wrap_instances(arr)
|
||||
T::InterfaceWrapper.wrap_instances(arr, self)
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method :new # use `wrap_instance`
|
||||
|
||||
def self.wrap_instance(obj, interface_mod)
|
||||
wrapper = wrapped_dynamic_cast(obj, interface_mod)
|
||||
if wrapper.nil?
|
||||
raise "#{obj.class} cannot be cast to #{interface_mod}"
|
||||
end
|
||||
wrapper
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
arr: Array,
|
||||
interface_mod: T.untyped
|
||||
)
|
||||
.returns(Array)
|
||||
end
|
||||
def self.wrap_instances(arr, interface_mod)
|
||||
arr.map {|instance| self.wrap_instance(instance, interface_mod)}
|
||||
end
|
||||
|
||||
def initialize(target_obj, interface_mod)
|
||||
if target_obj.is_a?(T::InterfaceWrapper)
|
||||
# wrapped_dynamic_cast should guarantee this never happens.
|
||||
raise "Unexpected: wrapping a wrapper. Please report this bug at https://github.com/sorbet/sorbet/issues"
|
||||
end
|
||||
|
||||
if !target_obj.is_a?(interface_mod)
|
||||
# wrapped_dynamic_cast should guarantee this never happens.
|
||||
raise "Unexpected: `is_a?` failed. Please report this bug at https://github.com/sorbet/sorbet/issues"
|
||||
end
|
||||
|
||||
if target_obj.class == interface_mod
|
||||
# wrapped_dynamic_cast should guarantee this never happens.
|
||||
raise "Unexpected: exact class match. Please report this bug at https://github.com/sorbet/sorbet/issues"
|
||||
end
|
||||
|
||||
@target_obj = target_obj
|
||||
@interface_mod = interface_mod
|
||||
self_methods = self.class.self_methods
|
||||
|
||||
# If perf becomes an issue, we can define these on an anonymous subclass, and keep a cache
|
||||
# so we only need to do it once per unique `interface_mod`
|
||||
T::Utils.methods_excluding_object(interface_mod).each do |method_name|
|
||||
if self_methods.include?(method_name)
|
||||
raise "interface_mod has a method that conflicts with #{self.class}: #{method_name}"
|
||||
end
|
||||
|
||||
define_singleton_method(method_name) do |*args, &blk|
|
||||
target_obj.send(method_name, *args, &blk)
|
||||
end
|
||||
|
||||
if target_obj.singleton_class.public_method_defined?(method_name)
|
||||
# no-op, it's already public
|
||||
elsif target_obj.singleton_class.protected_method_defined?(method_name)
|
||||
singleton_class.send(:protected, method_name)
|
||||
elsif target_obj.singleton_class.private_method_defined?(method_name)
|
||||
singleton_class.send(:private, method_name)
|
||||
else
|
||||
raise "This should never happen. Report this bug at https://github.com/sorbet/sorbet/issues"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def kind_of?(other)
|
||||
is_a?(other)
|
||||
end
|
||||
|
||||
def is_a?(other)
|
||||
if !other.is_a?(Module)
|
||||
raise TypeError.new("class or module required")
|
||||
end
|
||||
|
||||
# This makes is_a? return true for T::InterfaceWrapper (and its ancestors),
|
||||
# as well as for @interface_mod and its ancestors.
|
||||
self.class <= other || @interface_mod <= other
|
||||
end
|
||||
|
||||
# Prefixed because we're polluting the namespace of the interface we're wrapping, and we don't
|
||||
# want anyone else (besides dynamic_cast) calling it.
|
||||
def __target_obj_DO_NOT_USE # rubocop:disable Naming/MethodName
|
||||
@target_obj
|
||||
end
|
||||
|
||||
# Prefixed because we're polluting the namespace of the interface we're wrapping, and we don't
|
||||
# want anyone else (besides wrapped_dynamic_cast) calling it.
|
||||
def __interface_mod_DO_NOT_USE # rubocop:disable Naming/MethodName
|
||||
@interface_mod
|
||||
end
|
||||
|
||||
# "Cast" an object to another type. If `obj` is an InterfaceWrapper, returns the the wrapped
|
||||
# object if that matches `type`. Otherwise, returns `obj` if it matches `type`. Otherwise,
|
||||
# returns nil.
|
||||
#
|
||||
# @param obj [Object] object to cast
|
||||
# @param mod [Module] type to cast `obj` to
|
||||
#
|
||||
# @example
|
||||
# if (impl = T::InterfaceWrapper.dynamic_cast(iface, MyImplementation))
|
||||
# impl.do_things
|
||||
# end
|
||||
def self.dynamic_cast(obj, mod)
|
||||
if obj.is_a?(T::InterfaceWrapper)
|
||||
target_obj = obj.__target_obj_DO_NOT_USE
|
||||
target_obj.is_a?(mod) ? target_obj : nil
|
||||
elsif obj.is_a?(mod)
|
||||
obj
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Like dynamic_cast, but puts the result in its own wrapper if necessary.
|
||||
#
|
||||
# @param obj [Object] object to cast
|
||||
# @param mod [Module] type to cast `obj` to
|
||||
def self.wrapped_dynamic_cast(obj, mod)
|
||||
# Avoid unwrapping and creating an equivalent wrapper.
|
||||
if obj.is_a?(T::InterfaceWrapper) && obj.__interface_mod_DO_NOT_USE == mod
|
||||
return obj
|
||||
end
|
||||
|
||||
cast_obj = dynamic_cast(obj, mod)
|
||||
if cast_obj.nil?
|
||||
nil
|
||||
elsif cast_obj.class == mod
|
||||
# Nothing to wrap, they want the full class
|
||||
cast_obj
|
||||
else
|
||||
new(cast_obj, mod)
|
||||
end
|
||||
end
|
||||
|
||||
def self.self_methods
|
||||
@self_methods ||= self.instance_methods(false).to_set
|
||||
end
|
||||
end
|
@ -0,0 +1,65 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
module T::NonForcingConstants
|
||||
# NOTE: This method is documented on the RBI in Sorbet's payload, so that it
|
||||
# shows up in the hover/completion documentation via LSP.
|
||||
T::Sig::WithoutRuntime.sig {params(val: BasicObject, klass: String, package: T.nilable(String)).returns(T::Boolean)}
|
||||
def self.non_forcing_is_a?(val, klass, package: nil)
|
||||
method_name = "T::NonForcingConstants.non_forcing_is_a?"
|
||||
if klass.empty?
|
||||
raise ArgumentError.new("The string given to `#{method_name}` must not be empty")
|
||||
end
|
||||
|
||||
# We don't treat packages differently at runtime, but the static
|
||||
# type-checker still needs to have the package and constant
|
||||
# separated out. This just re-assembles the string as needed
|
||||
if !package.nil?
|
||||
klass = "::#{package}::#{klass}"
|
||||
end
|
||||
|
||||
current_klass = T.let(nil, T.nilable(Module))
|
||||
current_prefix = T.let(nil, T.nilable(String))
|
||||
|
||||
parts = klass.split('::')
|
||||
parts.each do |part|
|
||||
if current_klass.nil?
|
||||
# First iteration
|
||||
if part != "" && package.nil?
|
||||
# if we've supplied a package, we're probably running in
|
||||
# package mode, which means absolute references are
|
||||
# meaningless
|
||||
raise ArgumentError.new("The string given to `#{method_name}` must be an absolute constant reference that starts with `::`")
|
||||
end
|
||||
|
||||
current_klass = Object
|
||||
current_prefix = ''
|
||||
|
||||
# if this had a :: prefix, then there's no more loading to
|
||||
# do---skip to the next one
|
||||
next if part == ""
|
||||
end
|
||||
|
||||
if current_klass.autoload?(part)
|
||||
# There's an autoload registered for that constant, which means it's not
|
||||
# yet loaded. `value` can't be an instance of something not yet loaded.
|
||||
return false
|
||||
end
|
||||
|
||||
# Sorbet guarantees that the string is an absolutely resolved name.
|
||||
search_inheritance_chain = false
|
||||
if !current_klass.const_defined?(part, search_inheritance_chain)
|
||||
return false
|
||||
end
|
||||
|
||||
current_klass = current_klass.const_get(part)
|
||||
current_prefix = "#{current_prefix}::#{part}"
|
||||
|
||||
if !Module.===(current_klass)
|
||||
raise ArgumentError.new("#{current_prefix} is not a class or module")
|
||||
end
|
||||
end
|
||||
|
||||
current_klass.===(val)
|
||||
end
|
||||
end
|
@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
# We need to associate data with abstract modules. We could add instance methods to them that
|
||||
# access ivars, but those methods will unnecessarily pollute the module namespace, and they'd
|
||||
# have access to other private state and methods that they don't actually need. We also need to
|
||||
# associate data with arbitrary classes/modules that implement abstract mixins, where we don't
|
||||
# control the interface at all. So, we access data via these `get` and `set` methods.
|
||||
#
|
||||
# Using instance_variable_get/set here is gross, but the alternative is to use a hash keyed on
|
||||
# `mod`, and we can't trust that arbitrary modules can be added to those, because there are lurky
|
||||
# modules that override the `hash` method with something completely broken.
|
||||
module T::Private::Abstract::Data
|
||||
def self.get(mod, key)
|
||||
mod.instance_variable_get("@opus_abstract__#{key}") if key?(mod, key)
|
||||
end
|
||||
|
||||
def self.set(mod, key, value)
|
||||
mod.instance_variable_set("@opus_abstract__#{key}", value)
|
||||
end
|
||||
|
||||
def self.key?(mod, key)
|
||||
mod.instance_variable_defined?("@opus_abstract__#{key}")
|
||||
end
|
||||
|
||||
# Works like `setdefault` in Python. If key has already been set, return its value. If not,
|
||||
# insert `key` with a value of `default` and return `default`.
|
||||
def self.set_default(mod, key, default)
|
||||
if self.key?(mod, key)
|
||||
self.get(mod, key)
|
||||
else
|
||||
self.set(mod, key, default)
|
||||
default
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Private::Abstract::Declare
|
||||
Abstract = T::Private::Abstract
|
||||
AbstractUtils = T::AbstractUtils
|
||||
|
||||
def self.declare_abstract(mod, type:)
|
||||
if AbstractUtils.abstract_module?(mod)
|
||||
raise "#{mod} is already declared as abstract"
|
||||
end
|
||||
if T::Private::Final.final_module?(mod)
|
||||
raise "#{mod} was already declared as final and cannot be declared as abstract"
|
||||
end
|
||||
|
||||
Abstract::Data.set(mod, :can_have_abstract_methods, true)
|
||||
Abstract::Data.set(mod.singleton_class, :can_have_abstract_methods, true)
|
||||
Abstract::Data.set(mod, :abstract_type, type)
|
||||
|
||||
mod.extend(Abstract::Hooks)
|
||||
mod.extend(T::InterfaceWrapper::Helpers)
|
||||
|
||||
if mod.is_a?(Class)
|
||||
if type == :interface
|
||||
# Since `interface!` is just `abstract!` with some extra validation, we could technically
|
||||
# allow this, but it's unclear there are good use cases, and it might be confusing.
|
||||
raise "Classes can't be interfaces. Use `abstract!` instead of `interface!`."
|
||||
end
|
||||
|
||||
if mod.instance_method(:initialize).owner == mod
|
||||
raise "You must call `abstract!` *before* defining an initialize method"
|
||||
end
|
||||
|
||||
# Don't need to silence warnings via without_ruby_warnings when calling
|
||||
# define_method because of the guard above
|
||||
|
||||
mod.send(:define_method, :initialize) do |*args, &blk|
|
||||
if self.class == mod
|
||||
raise "#{mod} is declared as abstract; it cannot be instantiated"
|
||||
end
|
||||
super(*args, &blk)
|
||||
end
|
||||
|
||||
# Ruby doesn not emit "method redefined" warnings for aliased methods
|
||||
# (more robust than undef_method that would create a small window in which the method doesn't exist)
|
||||
mod.send(:alias_method, :initialize, :initialize)
|
||||
|
||||
if mod.respond_to?(:ruby2_keywords, true)
|
||||
mod.send(:ruby2_keywords, :initialize)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Private::Abstract::Hooks
|
||||
# This will become the self.extend_object method on a module that extends Abstract::Hooks.
|
||||
# It gets called when *that* module gets extended in another class/module (similar to the
|
||||
# `extended` hook, but this gets calls before the ancestors of `other` get modified, which
|
||||
# is important for our validation).
|
||||
private def extend_object(other)
|
||||
T::Private::Abstract::Data.set(self, :last_used_by, other)
|
||||
super
|
||||
end
|
||||
|
||||
# This will become the self.append_features method on a module that extends Abstract::Hooks.
|
||||
# It gets called when *that* module gets included in another class/module (similar to the
|
||||
# `included` hook, but this gets calls before the ancestors of `other` get modified, which
|
||||
# is important for our validation).
|
||||
private def append_features(other)
|
||||
T::Private::Abstract::Data.set(self, :last_used_by, other)
|
||||
super
|
||||
end
|
||||
|
||||
# This will become the self.inherited method on a class that extends Abstract::Hooks.
|
||||
# It gets called when *that* class gets inherited by another class.
|
||||
private def inherited(other)
|
||||
super
|
||||
# `self` may not actually be abstract -- it could be a concrete class that inherited from an
|
||||
# abstract class. We only need to check this in `inherited` because, for modules being included
|
||||
# or extended, the concrete ones won't have these hooks at all. This is just an optimization.
|
||||
return if !T::AbstractUtils.abstract_module?(self)
|
||||
|
||||
T::Private::Abstract::Data.set(self, :last_used_by, other)
|
||||
end
|
||||
|
||||
# This will become the self.prepended method on a module that extends Abstract::Hooks.
|
||||
# It will get called when *that* module gets prepended in another class/module.
|
||||
private def prepended(other)
|
||||
# Prepending abstract methods is weird. You'd only be able to override them via other prepended
|
||||
# modules, or in subclasses. Punt until we have a use case.
|
||||
Kernel.raise "Prepending abstract mixins is not currently supported."
|
||||
end
|
||||
end
|
@ -0,0 +1,128 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Private::Abstract::Validate
|
||||
Abstract = T::Private::Abstract
|
||||
AbstractUtils = T::AbstractUtils
|
||||
Methods = T::Private::Methods
|
||||
SignatureValidation = T::Private::Methods::SignatureValidation
|
||||
|
||||
def self.validate_abstract_module(mod)
|
||||
type = Abstract::Data.get(mod, :abstract_type)
|
||||
validate_interface(mod) if type == :interface
|
||||
end
|
||||
|
||||
# Validates a class/module with an abstract class/module as an ancestor. This must be called
|
||||
# after all methods on `mod` have been defined.
|
||||
def self.validate_subclass(mod)
|
||||
can_have_abstract_methods = !T::Private::Abstract::Data.get(mod, :can_have_abstract_methods)
|
||||
unimplemented_methods = []
|
||||
|
||||
T::AbstractUtils.declared_abstract_methods_for(mod).each do |abstract_method|
|
||||
implementation_method = mod.instance_method(abstract_method.name)
|
||||
if AbstractUtils.abstract_method?(implementation_method)
|
||||
# Note that when we end up here, implementation_method might not be the same as
|
||||
# abstract_method; the latter could've been overridden by another abstract method. In either
|
||||
# case, if we have a concrete definition in an ancestor, that will end up as the effective
|
||||
# implementation (see CallValidation.wrap_method_if_needed), so that's what we'll validate
|
||||
# against.
|
||||
implementation_method = T.unsafe(nil)
|
||||
mod.ancestors.each do |ancestor|
|
||||
if ancestor.instance_methods.include?(abstract_method.name)
|
||||
method = ancestor.instance_method(abstract_method.name)
|
||||
T::Private::Methods.maybe_run_sig_block_for_method(method)
|
||||
if !T::AbstractUtils.abstract_method?(method)
|
||||
implementation_method = method
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if !implementation_method
|
||||
# There's no implementation
|
||||
if can_have_abstract_methods
|
||||
unimplemented_methods << describe_method(abstract_method)
|
||||
end
|
||||
next # Nothing to validate
|
||||
end
|
||||
end
|
||||
|
||||
implementation_signature = Methods.signature_for_method(implementation_method)
|
||||
# When a signature exists and the method is defined directly on `mod`, we skip the validation
|
||||
# here, because it will have already been done when the method was defined (by
|
||||
# T::Private::Methods._on_method_added).
|
||||
next if implementation_signature&.owner == mod
|
||||
|
||||
# We validate the remaining cases here: (a) methods defined directly on `mod` without a
|
||||
# signature and (b) methods from ancestors (note that these ancestors can come before or
|
||||
# after the abstract module in the inheritance chain -- the former coming from
|
||||
# walking `mod.ancestors` above).
|
||||
abstract_signature = Methods.signature_for_method(abstract_method)
|
||||
# We allow implementation methods to be defined without a signature.
|
||||
# In that case, get its untyped signature.
|
||||
implementation_signature ||= Methods::Signature.new_untyped(
|
||||
method: implementation_method,
|
||||
mode: Methods::Modes.override
|
||||
)
|
||||
SignatureValidation.validate_override_shape(implementation_signature, abstract_signature)
|
||||
SignatureValidation.validate_override_types(implementation_signature, abstract_signature)
|
||||
end
|
||||
|
||||
method_type = mod.singleton_class? ? "class" : "instance"
|
||||
if !unimplemented_methods.empty?
|
||||
raise "Missing implementation for abstract #{method_type} method(s) in #{mod}:\n" \
|
||||
"#{unimplemented_methods.join("\n")}\n" \
|
||||
"If #{mod} is meant to be an abstract class/module, you can call " \
|
||||
"`abstract!` or `interface!`. Otherwise, you must implement the method(s)."
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method def self.validate_interface_all_abstract(mod, method_names)
|
||||
violations = method_names.map do |method_name|
|
||||
method = mod.instance_method(method_name)
|
||||
if !AbstractUtils.abstract_method?(method)
|
||||
describe_method(method, show_owner: false)
|
||||
end
|
||||
end.compact
|
||||
|
||||
if !violations.empty?
|
||||
raise "`#{mod}` is declared as an interface, but the following methods are not declared " \
|
||||
"with `abstract`:\n#{violations.join("\n")}"
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method def self.validate_interface(mod)
|
||||
interface_methods = T::Utils.methods_excluding_object(mod)
|
||||
validate_interface_all_abstract(mod, interface_methods)
|
||||
validate_interface_all_public(mod, interface_methods)
|
||||
end
|
||||
|
||||
private_class_method def self.validate_interface_all_public(mod, method_names)
|
||||
violations = method_names.map do |method_name|
|
||||
if !mod.public_method_defined?(method_name)
|
||||
describe_method(mod.instance_method(method_name), show_owner: false)
|
||||
end
|
||||
end.compact
|
||||
|
||||
if !violations.empty?
|
||||
raise "All methods on an interface must be public. If you intend to have non-public " \
|
||||
"methods, declare your class/module using `abstract!` instead of `interface!`. " \
|
||||
"The following methods on `#{mod}` are not public: \n#{violations.join("\n")}"
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method def self.describe_method(method, show_owner: true)
|
||||
loc = if method.source_location
|
||||
method.source_location.join(':')
|
||||
else
|
||||
"<unknown location>"
|
||||
end
|
||||
|
||||
owner = if show_owner
|
||||
" declared in #{method.owner}"
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
" * `#{method.name}`#{owner} at #{loc}"
|
||||
end
|
||||
end
|
@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Private
|
||||
module Casts
|
||||
def self.cast(value, type, cast_method:)
|
||||
begin
|
||||
error = T::Utils.coerce(type).error_message_for_obj(value)
|
||||
return value unless error
|
||||
|
||||
caller_loc = T.must(caller_locations(2..2)).first
|
||||
|
||||
suffix = "Caller: #{T.must(caller_loc).path}:#{T.must(caller_loc).lineno}"
|
||||
|
||||
raise TypeError.new("#{cast_method}: #{error}\n#{suffix}")
|
||||
rescue TypeError => e # raise into rescue to ensure e.backtrace is populated
|
||||
T::Configuration.inline_type_error_handler(e, {kind: cast_method, value: value, type: type})
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
# there's a lot of shared logic with the above one, but factoring
|
||||
# it out like this makes it easier to hopefully one day delete
|
||||
# this one
|
||||
def self.cast_recursive(value, type, cast_method:)
|
||||
begin
|
||||
error = T::Utils.coerce(type).error_message_for_obj_recursive(value)
|
||||
return value unless error
|
||||
|
||||
caller_loc = T.must(caller_locations(2..2)).first
|
||||
|
||||
suffix = "Caller: #{T.must(caller_loc).path}:#{T.must(caller_loc).lineno}"
|
||||
|
||||
raise TypeError.new("#{cast_method}: #{error}\n#{suffix}")
|
||||
rescue TypeError => e # raise into rescue to ensure e.backtrace is populated
|
||||
T::Configuration.inline_type_error_handler(e, {kind: cast_method, value: value, type: type})
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,110 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
# Cut down version of Chalk::Tools::ClassUtils with only :replace_method functionality.
|
||||
# Extracted to a separate namespace so the type system can be used standalone.
|
||||
module T::Private::ClassUtils
|
||||
class ReplacedMethod
|
||||
def initialize(mod, old_method, new_method, overwritten, visibility)
|
||||
if old_method.name != new_method.name
|
||||
raise "Method names must match. old=#{old_method.name} new=#{new_method.name}"
|
||||
end
|
||||
@mod = mod
|
||||
@old_method = old_method
|
||||
@new_method = new_method
|
||||
@overwritten = overwritten
|
||||
@name = old_method.name
|
||||
@visibility = visibility
|
||||
@restored = false
|
||||
end
|
||||
|
||||
def restore
|
||||
# The check below would also catch this, but this makes the failure mode much clearer
|
||||
if @restored
|
||||
raise "Method '#{@name}' on '#{@mod}' was already restored"
|
||||
end
|
||||
|
||||
if @mod.instance_method(@name) != @new_method
|
||||
raise "Trying to restore #{@mod}##{@name} but the method has changed since the call to replace_method"
|
||||
end
|
||||
|
||||
@restored = true
|
||||
|
||||
if @overwritten
|
||||
# The original method was overwritten. Overwrite again to restore it.
|
||||
T::Configuration.without_ruby_warnings do
|
||||
@mod.send(:define_method, @old_method.name, @old_method)
|
||||
end
|
||||
else
|
||||
# The original method was in an ancestor. Restore it by removing the overriding method.
|
||||
@mod.send(:remove_method, @old_method.name)
|
||||
end
|
||||
|
||||
# Restore the visibility. Note that we need to do this even when we call remove_method
|
||||
# above, because the module may have set custom visibility for a method it inherited.
|
||||
@mod.send(@visibility, @old_method.name)
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def bind(obj)
|
||||
@old_method.bind(obj)
|
||||
end
|
||||
|
||||
def to_s
|
||||
@old_method.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# `name` must be an instance method (for class methods, pass in mod.singleton_class)
|
||||
private_class_method def self.visibility_method_name(mod, name)
|
||||
if mod.public_method_defined?(name)
|
||||
:public
|
||||
elsif mod.protected_method_defined?(name)
|
||||
:protected
|
||||
elsif mod.private_method_defined?(name)
|
||||
:private
|
||||
else
|
||||
raise NameError.new("undefined method `#{name}` for `#{mod}`")
|
||||
end
|
||||
end
|
||||
|
||||
# Replaces a method, either by overwriting it (if it is defined directly on `mod`) or by
|
||||
# overriding it (if it is defined by one of mod's ancestors). Returns a ReplacedMethod instance
|
||||
# on which you can call `bind(...).call(...)` to call the original method, or `restore` to
|
||||
# restore the original method (by overwriting or removing the override).
|
||||
def self.replace_method(mod, name, &blk)
|
||||
original_method = mod.instance_method(name)
|
||||
original_visibility = visibility_method_name(mod, name)
|
||||
original_owner = original_method.owner
|
||||
|
||||
mod.ancestors.each do |ancestor|
|
||||
break if ancestor == mod
|
||||
if ancestor == original_owner
|
||||
# If we get here, that means the method we're trying to replace exists on a *prepended*
|
||||
# mixin, which means in order to supersede it, we'd need to create a method on a new
|
||||
# module that we'd prepend before `ancestor`. The problem with that approach is there'd
|
||||
# be no way to remove that new module after prepending it, so we'd be left with these
|
||||
# empty anonymous modules in the ancestor chain after calling `restore`.
|
||||
#
|
||||
# That's not necessarily a deal breaker, but for now, we're keeping it as unsupported.
|
||||
raise "You're trying to replace `#{name}` on `#{mod}`, but that method exists in a " \
|
||||
"prepended module (#{ancestor}), which we don't currently support."
|
||||
end
|
||||
end
|
||||
|
||||
overwritten = original_owner == mod
|
||||
T::Configuration.without_ruby_warnings do
|
||||
T::Private::DeclState.current.without_on_method_added do
|
||||
mod.send(:define_method, name, &blk)
|
||||
if blk.arity < 0 && mod.respond_to?(:ruby2_keywords, true)
|
||||
mod.send(:ruby2_keywords, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
mod.send(original_visibility, name)
|
||||
new_method = mod.instance_method(name)
|
||||
|
||||
ReplacedMethod.new(mod, original_method, new_method, overwritten, original_visibility)
|
||||
end
|
||||
end
|
@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Private
|
||||
module Compiler
|
||||
# If this code ever runs, the caller is running interpreted (or the
|
||||
# compiler didn't see the call to `running_compiled?` statically.)
|
||||
#
|
||||
# The Sorbet Compiler replaces calls to this method unconditionally (no
|
||||
# runtime guards) to return `true` when compiling a file.
|
||||
def self.running_compiled?
|
||||
false
|
||||
end
|
||||
|
||||
# Returns `nil` because the compiler isn't running.
|
||||
#
|
||||
# The Sorbet Compiler replaces calls to this method unconditionally (no
|
||||
# runtime guards) to return a String showing the Sorbet Compiler's version
|
||||
# string.
|
||||
def self.compiler_version
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
class T::Private::DeclState
|
||||
def self.current
|
||||
Thread.current[:opus_types__decl_state] ||= self.new
|
||||
end
|
||||
|
||||
def self.current=(other)
|
||||
Thread.current[:opus_types__decl_state] = other
|
||||
end
|
||||
|
||||
attr_accessor :active_declaration
|
||||
attr_accessor :skip_on_method_added
|
||||
|
||||
def reset!
|
||||
self.active_declaration = nil
|
||||
end
|
||||
|
||||
def without_on_method_added
|
||||
begin
|
||||
# explicit 'self' is needed here
|
||||
old_value = self.skip_on_method_added
|
||||
self.skip_on_method_added = true
|
||||
yield
|
||||
ensure
|
||||
self.skip_on_method_added = old_value
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Private::Final
|
||||
module NoInherit
|
||||
def inherited(arg)
|
||||
super(arg)
|
||||
raise "#{self} was declared as final and cannot be inherited"
|
||||
end
|
||||
end
|
||||
|
||||
module NoIncludeExtend
|
||||
def included(arg)
|
||||
super(arg)
|
||||
raise "#{self} was declared as final and cannot be included"
|
||||
end
|
||||
|
||||
def extended(arg)
|
||||
super(arg)
|
||||
raise "#{self} was declared as final and cannot be extended"
|
||||
end
|
||||
end
|
||||
|
||||
def self.declare(mod)
|
||||
if !mod.is_a?(Module)
|
||||
raise "#{mod} is not a class or module and cannot be declared as final with `final!`"
|
||||
end
|
||||
if final_module?(mod)
|
||||
raise "#{mod} was already declared as final and cannot be re-declared as final"
|
||||
end
|
||||
if T::AbstractUtils.abstract_module?(mod)
|
||||
raise "#{mod} was already declared as abstract and cannot be declared as final"
|
||||
end
|
||||
if T::Private::Sealed.sealed_module?(mod)
|
||||
raise "#{mod} was already declared as sealed and cannot be declared as final"
|
||||
end
|
||||
mod.extend(mod.is_a?(Class) ? NoInherit : NoIncludeExtend)
|
||||
mark_as_final_module(mod)
|
||||
mark_as_final_module(mod.singleton_class)
|
||||
T::Private::Methods.install_hooks(mod)
|
||||
end
|
||||
|
||||
def self.final_module?(mod)
|
||||
mod.instance_variable_defined?(:@sorbet_final_module)
|
||||
end
|
||||
|
||||
private_class_method def self.mark_as_final_module(mod)
|
||||
mod.instance_variable_set(:@sorbet_final_module, true)
|
||||
end
|
||||
end
|
@ -0,0 +1,581 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Private::Methods
|
||||
@installed_hooks = {}
|
||||
@signatures_by_method = {}
|
||||
@sig_wrappers = {}
|
||||
@sigs_that_raised = {}
|
||||
# stores method names that were declared final without regard for where.
|
||||
# enables early rejection of names that we know can't induce final method violations.
|
||||
@was_ever_final_names = {}
|
||||
# maps from a module's object_id to the set of final methods declared in that module.
|
||||
# we also overload entries slightly: if the value is nil, that means that the
|
||||
# module has final methods somewhere along its ancestor chain, but does not itself
|
||||
# have any final methods.
|
||||
#
|
||||
# we need the latter information to know whether we need to check along the ancestor
|
||||
# chain for final method violations. we need the former information because we
|
||||
# care about exactly where a final method is defined (e.g. including the same module
|
||||
# twice is permitted). we could do this with two tables, but it seems slightly
|
||||
# cleaner with a single table.
|
||||
# Effectively T::Hash[Module, T.nilable(Set))]
|
||||
@modules_with_final = Hash.new {|hash, key| hash[key] = nil}
|
||||
# this stores the old [included, extended] hooks for Module and inherited hook for Class that we override when
|
||||
# enabling final checks for when those hooks are called. the 'hooks' here don't have anything to do with the 'hooks'
|
||||
# in installed_hooks.
|
||||
@old_hooks = nil
|
||||
|
||||
ARG_NOT_PROVIDED = Object.new
|
||||
PROC_TYPE = Object.new
|
||||
|
||||
DeclarationBlock = Struct.new(:mod, :loc, :blk, :final, :raw)
|
||||
|
||||
def self.declare_sig(mod, loc, arg, &blk)
|
||||
T::Private::DeclState.current.active_declaration = _declare_sig_internal(mod, loc, arg, &blk)
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
# See tests for how to use this. But you shouldn't be using this.
|
||||
def self._declare_sig(mod, arg=nil, &blk)
|
||||
_declare_sig_internal(mod, caller_locations(1, 1).first, arg, raw: true, &blk)
|
||||
end
|
||||
|
||||
private_class_method def self._declare_sig_internal(mod, loc, arg, raw: false, &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 in between"
|
||||
end
|
||||
|
||||
if !arg.nil? && arg != :final
|
||||
raise "Invalid argument to `sig`: #{arg}"
|
||||
end
|
||||
|
||||
DeclarationBlock.new(mod, loc, blk, arg == :final, raw)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def self.start_proc
|
||||
DeclBuilder.new(PROC_TYPE, false)
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
# Returns the signature for a method whose definition was preceded by `sig`.
|
||||
#
|
||||
# @param method [UnboundMethod]
|
||||
# @return [T::Private::Methods::Signature]
|
||||
def self.signature_for_method(method)
|
||||
signature_for_key(method_to_key(method))
|
||||
end
|
||||
|
||||
private_class_method def self.signature_for_key(key)
|
||||
maybe_run_sig_block_for_key(key)
|
||||
|
||||
# If a subclass Sub inherits a method `foo` from Base, then
|
||||
# Sub.instance_method(:foo) != Base.instance_method(:foo) even though they resolve to the
|
||||
# same method. Similarly, Foo.method(:bar) != Foo.singleton_class.instance_method(:bar).
|
||||
# So, we always do the look up by the method on the owner (Base in this example).
|
||||
@signatures_by_method[key]
|
||||
end
|
||||
|
||||
# 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.
|
||||
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.object_id, 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.object_id, 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.object_id).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 = caller_locations&.find {|l| !l.to_s.match?(%r{sorbet-runtime[^/]*/lib/})}
|
||||
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
|
||||
|
||||
def self.add_module_with_final_method(mod, method_name, is_singleton_method)
|
||||
m = is_singleton_method ? mod.singleton_class : mod
|
||||
mid = m.object_id
|
||||
methods = @modules_with_final[mid]
|
||||
if methods.nil?
|
||||
methods = {}
|
||||
@modules_with_final[mid] = methods
|
||||
end
|
||||
methods[method_name] = true
|
||||
nil
|
||||
end
|
||||
|
||||
def self.note_module_deals_with_final(mod)
|
||||
# Side-effectfully initialize the value if it's not already there
|
||||
@modules_with_final[mod.object_id]
|
||||
@modules_with_final[mod.singleton_class.object_id]
|
||||
end
|
||||
|
||||
# Only public because it needs to get called below inside the replace_method blocks below.
|
||||
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
|
||||
# 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.object_id)
|
||||
_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) 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)
|
||||
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(hook_mod, method_name, is_singleton_method)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
# Executes the `sig` block, and converts the resulting Declaration
|
||||
# to a Signature.
|
||||
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(signature.method.owner, signature, original_method)
|
||||
signature
|
||||
end
|
||||
|
||||
def self.run_builder(declaration_block)
|
||||
builder = DeclBuilder.new(declaration_block.mod, declaration_block.raw)
|
||||
builder
|
||||
.instance_exec(&declaration_block.blk)
|
||||
.finalize!
|
||||
.decl
|
||||
end
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def self.has_sig_block_for_method(method)
|
||||
has_sig_block_for_key(method_to_key(method))
|
||||
end
|
||||
|
||||
private_class_method def self.has_sig_block_for_key(key)
|
||||
@sig_wrappers.key?(key)
|
||||
end
|
||||
|
||||
def self.maybe_run_sig_block_for_method(method)
|
||||
maybe_run_sig_block_for_key(method_to_key(method))
|
||||
end
|
||||
|
||||
# Only public so that it can be accessed in the closure for _on_method_added
|
||||
def self.maybe_run_sig_block_for_key(key)
|
||||
run_sig_block_for_key(key) if has_sig_block_for_key(key)
|
||||
end
|
||||
|
||||
def self.run_sig_block_for_method(method)
|
||||
run_sig_block_for_key(method_to_key(method))
|
||||
end
|
||||
|
||||
private_class_method def self.run_sig_block_for_key(key)
|
||||
blk = @sig_wrappers[key]
|
||||
if !blk
|
||||
sig = @signatures_by_method[key]
|
||||
if sig
|
||||
# We already ran the sig block, perhaps in another thread.
|
||||
return sig
|
||||
else
|
||||
raise "No `sig` wrapper for #{key_to_method(key)}"
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
sig = blk.call
|
||||
rescue
|
||||
@sigs_that_raised[key] = true
|
||||
raise
|
||||
end
|
||||
if @sigs_that_raised[key]
|
||||
raise "A previous invocation of #{key_to_method(key)} raised, and the current one succeeded. Please don't do that."
|
||||
end
|
||||
|
||||
@sig_wrappers.delete(key)
|
||||
sig
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def self.all_checked_tests_sigs
|
||||
@signatures_by_method.values.select {|sig| sig.check_level == :tests}
|
||||
end
|
||||
|
||||
# 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.
|
||||
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.object_id)
|
||||
if !@modules_with_final.include?(source.object_id)
|
||||
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
|
||||
|
||||
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, false, 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, true, 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, false, self)
|
||||
end
|
||||
@old_hooks = [old_included, old_extended, old_inherited]
|
||||
end
|
||||
end
|
||||
|
||||
module MethodHooks
|
||||
def method_added(name)
|
||||
super(name)
|
||||
::T::Private::Methods._on_method_added(self, name, is_singleton_method: false)
|
||||
end
|
||||
end
|
||||
|
||||
module SingletonMethodHooks
|
||||
def singleton_method_added(name)
|
||||
super(name)
|
||||
::T::Private::Methods._on_method_added(self, name, is_singleton_method: true)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
# use this directly if you don't want/need to box up the method into an object to pass to method_to_key.
|
||||
private_class_method def self.method_owner_and_name_to_key(owner, name)
|
||||
"#{owner.object_id}##{name}"
|
||||
end
|
||||
|
||||
private_class_method def self.method_to_key(method)
|
||||
method_owner_and_name_to_key(method.owner, method.name)
|
||||
end
|
||||
|
||||
private_class_method def self.key_to_method(key)
|
||||
id, name = key.split("#")
|
||||
obj = ObjectSpace._id2ref(id.to_i)
|
||||
obj.instance_method(name)
|
||||
end
|
||||
end
|
||||
|
||||
# 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`
|
||||
T::Private::Methods::TOP_SELF = self
|
@ -0,0 +1,221 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Private::Methods::CallValidation
|
||||
CallValidation = T::Private::Methods::CallValidation
|
||||
Modes = T::Private::Methods::Modes
|
||||
|
||||
# Wraps a method with a layer of validation for the given type signature.
|
||||
# This wrapper is meant to be fast, and is applied by a previous wrapper,
|
||||
# which was placed by `_on_method_added`.
|
||||
#
|
||||
# @param method_sig [T::Private::Methods::Signature]
|
||||
# @return [UnboundMethod] the new wrapper method (or the original one if we didn't wrap it)
|
||||
def self.wrap_method_if_needed(mod, method_sig, original_method)
|
||||
original_visibility = visibility_method_name(mod, method_sig.method_name)
|
||||
if method_sig.mode == T::Private::Methods::Modes.abstract
|
||||
T::Private::ClassUtils.replace_method(mod, method_sig.method_name) do |*args, &blk|
|
||||
# TODO: write a cop to ensure that abstract methods have an empty body
|
||||
#
|
||||
# We allow abstract methods to be implemented by things further down the ancestor chain.
|
||||
# So, if a super method exists, call it.
|
||||
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
|
||||
end
|
||||
# Do nothing in this case; this method was not wrapped in _on_method_added.
|
||||
elsif method_sig.defined_raw
|
||||
# Note, this logic is duplicated (intentionally, for micro-perf) at `Methods._on_method_added`,
|
||||
# make sure to keep changes in sync.
|
||||
# This is a trapdoor point for each method:
|
||||
# if a given method is wrapped, it stays wrapped; and if not, it's never wrapped.
|
||||
# (Therefore, we need the `@wrapped_tests_with_validation` check in `T::RuntimeLevels`.)
|
||||
elsif method_sig.check_level == :always || (method_sig.check_level == :tests && T::Private::RuntimeLevels.check_tests?)
|
||||
create_validator_method(mod, original_method, method_sig, original_visibility)
|
||||
else
|
||||
T::Configuration.without_ruby_warnings do
|
||||
# get all the shims out of the way and put back the original method
|
||||
T::Private::DeclState.current.without_on_method_added do
|
||||
mod.send(:define_method, method_sig.method_name, original_method)
|
||||
end
|
||||
mod.send(original_visibility, method_sig.method_name)
|
||||
end
|
||||
end
|
||||
# Return the newly created method (or the original one if we didn't replace it)
|
||||
mod.instance_method(method_sig.method_name)
|
||||
end
|
||||
|
||||
@is_allowed_to_have_fast_path = true
|
||||
def self.is_allowed_to_have_fast_path
|
||||
@is_allowed_to_have_fast_path
|
||||
end
|
||||
|
||||
def self.disable_fast_path
|
||||
@is_allowed_to_have_fast_path = false
|
||||
end
|
||||
|
||||
def self.create_validator_method(mod, original_method, method_sig, original_visibility)
|
||||
has_fixed_arity = method_sig.kwarg_types.empty? && !method_sig.has_rest && !method_sig.has_keyrest &&
|
||||
original_method.parameters.all? {|(kind, _name)| kind == :req}
|
||||
ok_for_fast_path = has_fixed_arity && !method_sig.bind && method_sig.arg_types.length < 5 && is_allowed_to_have_fast_path
|
||||
|
||||
all_args_are_simple = ok_for_fast_path && method_sig.arg_types.all? {|_name, type| type.is_a?(T::Types::Simple)}
|
||||
simple_method = all_args_are_simple && method_sig.return_type.is_a?(T::Types::Simple)
|
||||
simple_procedure = all_args_are_simple && method_sig.return_type.is_a?(T::Private::Types::Void)
|
||||
|
||||
T::Configuration.without_ruby_warnings do
|
||||
T::Private::DeclState.current.without_on_method_added do
|
||||
if simple_method
|
||||
create_validator_method_fast(mod, original_method, method_sig)
|
||||
elsif simple_procedure
|
||||
create_validator_procedure_fast(mod, original_method, method_sig)
|
||||
elsif ok_for_fast_path && method_sig.return_type.is_a?(T::Private::Types::Void)
|
||||
create_validator_procedure_medium(mod, original_method, method_sig)
|
||||
elsif ok_for_fast_path
|
||||
create_validator_method_medium(mod, original_method, method_sig)
|
||||
else
|
||||
create_validator_slow(mod, original_method, method_sig)
|
||||
end
|
||||
end
|
||||
end
|
||||
mod.send(original_visibility, method_sig.method_name)
|
||||
end
|
||||
|
||||
def self.create_validator_slow(mod, original_method, method_sig)
|
||||
mod.send(:define_method, method_sig.method_name) do |*args, &blk|
|
||||
CallValidation.validate_call(self, original_method, method_sig, args, blk)
|
||||
end
|
||||
if mod.respond_to?(:ruby2_keywords, true)
|
||||
mod.send(:ruby2_keywords, method_sig.method_name)
|
||||
end
|
||||
end
|
||||
|
||||
def self.validate_call(instance, original_method, method_sig, args, blk)
|
||||
# This method is called for every `sig`. It's critical to keep it fast and
|
||||
# reduce number of allocations that happen here.
|
||||
|
||||
if method_sig.bind
|
||||
message = method_sig.bind.error_message_for_obj(instance)
|
||||
if message
|
||||
CallValidation.report_error(
|
||||
method_sig,
|
||||
message,
|
||||
'Bind',
|
||||
nil,
|
||||
method_sig.bind,
|
||||
instance
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# NOTE: We don't bother validating for missing or extra kwargs;
|
||||
# the method call itself will take care of that.
|
||||
method_sig.each_args_value_type(args) do |name, arg, type|
|
||||
message = type.error_message_for_obj(arg)
|
||||
if message
|
||||
CallValidation.report_error(
|
||||
method_sig,
|
||||
message,
|
||||
'Parameter',
|
||||
name,
|
||||
type,
|
||||
arg,
|
||||
caller_offset: 2
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if method_sig.block_type
|
||||
message = method_sig.block_type.error_message_for_obj(blk)
|
||||
if message
|
||||
CallValidation.report_error(
|
||||
method_sig,
|
||||
message,
|
||||
'Block parameter',
|
||||
method_sig.block_name,
|
||||
method_sig.block_type,
|
||||
blk
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# The following line breaks are intentional to show nice pry message
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# PRY note:
|
||||
# this code is sig validation code.
|
||||
# Please issue `finish` to step out of it
|
||||
|
||||
return_value = T::Configuration::AT_LEAST_RUBY_2_7 ? original_method.bind_call(instance, *args, &blk) : original_method.bind(instance).call(*args, &blk)
|
||||
|
||||
# The only type that is allowed to change the return value is `.void`.
|
||||
# It ignores what you returned and changes it to be a private singleton.
|
||||
if method_sig.return_type.is_a?(T::Private::Types::Void)
|
||||
T::Private::Types::Void::VOID
|
||||
else
|
||||
message = method_sig.return_type.error_message_for_obj(return_value)
|
||||
if message
|
||||
CallValidation.report_error(
|
||||
method_sig,
|
||||
message,
|
||||
'Return value',
|
||||
nil,
|
||||
method_sig.return_type,
|
||||
return_value,
|
||||
)
|
||||
end
|
||||
return_value
|
||||
end
|
||||
end
|
||||
|
||||
def self.report_error(method_sig, error_message, kind, name, type, value, caller_offset: 0)
|
||||
caller_loc = T.must(caller_locations(3 + caller_offset, 1))[0]
|
||||
definition_file, definition_line = method_sig.method.source_location
|
||||
|
||||
pretty_message = "#{kind}#{name ? " '#{name}'" : ''}: #{error_message}\n" \
|
||||
"Caller: #{caller_loc.path}:#{caller_loc.lineno}\n" \
|
||||
"Definition: #{definition_file}:#{definition_line}"
|
||||
|
||||
T::Configuration.call_validation_error_handler(
|
||||
method_sig,
|
||||
message: error_message,
|
||||
pretty_message: pretty_message,
|
||||
kind: kind,
|
||||
name: name,
|
||||
type: type,
|
||||
value: value,
|
||||
location: caller_loc
|
||||
)
|
||||
end
|
||||
|
||||
# `name` must be an instance method (for class methods, pass in mod.singleton_class)
|
||||
private_class_method def self.visibility_method_name(mod, name)
|
||||
if mod.public_method_defined?(name)
|
||||
:public
|
||||
elsif mod.protected_method_defined?(name)
|
||||
:protected
|
||||
elsif mod.private_method_defined?(name)
|
||||
:private
|
||||
else
|
||||
raise NameError.new("undefined method `#{name}` for `#{mod}`")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if T::Configuration::AT_LEAST_RUBY_2_7
|
||||
require_relative './call_validation_2_7'
|
||||
else
|
||||
require_relative './call_validation_2_6'
|
||||
end
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,232 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Private::Methods
|
||||
Declaration = Struct.new(:mod, :params, :returns, :bind, :mode, :checked, :finalized, :on_failure, :override_allow_incompatible, :type_parameters, :raw)
|
||||
|
||||
class DeclBuilder
|
||||
attr_reader :decl
|
||||
|
||||
class BuilderError < StandardError; end
|
||||
|
||||
private def check_live!
|
||||
if decl.finalized
|
||||
raise BuilderError.new("You can't modify a signature declaration after it has been used.")
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(mod, raw)
|
||||
# TODO RUBYPLAT-1278 - with ruby 2.5, use kwargs here
|
||||
@decl = Declaration.new(
|
||||
mod,
|
||||
ARG_NOT_PROVIDED, # params
|
||||
ARG_NOT_PROVIDED, # returns
|
||||
ARG_NOT_PROVIDED, # bind
|
||||
Modes.standard, # mode
|
||||
ARG_NOT_PROVIDED, # checked
|
||||
false, # finalized
|
||||
ARG_NOT_PROVIDED, # on_failure
|
||||
nil, # override_allow_incompatible
|
||||
ARG_NOT_PROVIDED, # type_parameters
|
||||
raw
|
||||
)
|
||||
end
|
||||
|
||||
def params(**params)
|
||||
check_live!
|
||||
if !decl.params.equal?(ARG_NOT_PROVIDED)
|
||||
raise BuilderError.new("You can't call .params twice")
|
||||
end
|
||||
|
||||
if params.empty?
|
||||
raise BuilderError.new("params expects keyword arguments")
|
||||
end
|
||||
decl.params = params
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def returns(type)
|
||||
check_live!
|
||||
if decl.returns.is_a?(T::Private::Types::Void)
|
||||
raise BuilderError.new("You can't call .returns after calling .void.")
|
||||
end
|
||||
if !decl.returns.equal?(ARG_NOT_PROVIDED)
|
||||
raise BuilderError.new("You can't call .returns multiple times in a signature.")
|
||||
end
|
||||
|
||||
decl.returns = type
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def void
|
||||
check_live!
|
||||
if !decl.returns.equal?(ARG_NOT_PROVIDED)
|
||||
raise BuilderError.new("You can't call .void after calling .returns.")
|
||||
end
|
||||
|
||||
decl.returns = T::Private::Types::Void.new
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def bind(type)
|
||||
check_live!
|
||||
if !decl.bind.equal?(ARG_NOT_PROVIDED)
|
||||
raise BuilderError.new("You can't call .bind multiple times in a signature.")
|
||||
end
|
||||
|
||||
decl.bind = type
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def checked(level)
|
||||
check_live!
|
||||
|
||||
if !decl.checked.equal?(ARG_NOT_PROVIDED)
|
||||
raise BuilderError.new("You can't call .checked multiple times in a signature.")
|
||||
end
|
||||
if (level == :never || level == :compiled) && !decl.on_failure.equal?(ARG_NOT_PROVIDED)
|
||||
raise BuilderError.new("You can't use .checked(:#{level}) with .on_failure because .on_failure will have no effect.")
|
||||
end
|
||||
if !T::Private::RuntimeLevels::LEVELS.include?(level)
|
||||
raise BuilderError.new("Invalid `checked` level '#{level}'. Use one of: #{T::Private::RuntimeLevels::LEVELS}.")
|
||||
end
|
||||
|
||||
decl.checked = level
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def on_failure(*args)
|
||||
check_live!
|
||||
|
||||
if !decl.on_failure.equal?(ARG_NOT_PROVIDED)
|
||||
raise BuilderError.new("You can't call .on_failure multiple times in a signature.")
|
||||
end
|
||||
if decl.checked == :never || decl.checked == :compiled
|
||||
raise BuilderError.new("You can't use .on_failure with .checked(:#{decl.checked}) because .on_failure will have no effect.")
|
||||
end
|
||||
|
||||
decl.on_failure = args
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def abstract
|
||||
check_live!
|
||||
|
||||
case decl.mode
|
||||
when Modes.standard
|
||||
decl.mode = Modes.abstract
|
||||
when Modes.abstract
|
||||
raise BuilderError.new(".abstract cannot be repeated in a single signature")
|
||||
else
|
||||
raise BuilderError.new("`.abstract` cannot be combined with `.override` or `.overridable`.")
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def final
|
||||
check_live!
|
||||
raise BuilderError.new("The syntax for declaring a method final is `sig(:final) {...}`, not `sig {final. ...}`")
|
||||
end
|
||||
|
||||
def override(allow_incompatible: false)
|
||||
check_live!
|
||||
|
||||
case decl.mode
|
||||
when Modes.standard
|
||||
decl.mode = Modes.override
|
||||
decl.override_allow_incompatible = allow_incompatible
|
||||
when Modes.override, Modes.overridable_override
|
||||
raise BuilderError.new(".override cannot be repeated in a single signature")
|
||||
when Modes.overridable
|
||||
decl.mode = Modes.overridable_override
|
||||
else
|
||||
raise BuilderError.new("`.override` cannot be combined with `.abstract`.")
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def overridable
|
||||
check_live!
|
||||
|
||||
case decl.mode
|
||||
when Modes.abstract
|
||||
raise BuilderError.new("`.overridable` cannot be combined with `.#{decl.mode}`")
|
||||
when Modes.override
|
||||
decl.mode = Modes.overridable_override
|
||||
when Modes.standard
|
||||
decl.mode = Modes.overridable
|
||||
when Modes.overridable, Modes.overridable_override
|
||||
raise BuilderError.new(".overridable cannot be repeated in a single signature")
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# Declares valid type paramaters which can be used with `T.type_parameter` in
|
||||
# this `sig`.
|
||||
#
|
||||
# This is used for generic methods. Example usage:
|
||||
#
|
||||
# sig do
|
||||
# type_parameters(:U)
|
||||
# .params(blk: T.proc.params(arg0: Elem).returns(T.type_parameter(:U)))
|
||||
# .returns(T::Array[T.type_parameter(:U)])
|
||||
# end
|
||||
# def map(&blk); end
|
||||
def type_parameters(*names)
|
||||
check_live!
|
||||
|
||||
names.each do |name|
|
||||
raise BuilderError.new("not a symbol: #{name}") unless name.is_a?(Symbol)
|
||||
end
|
||||
|
||||
if !decl.type_parameters.equal?(ARG_NOT_PROVIDED)
|
||||
raise BuilderError.new("You can't call .type_parameters multiple times in a signature.")
|
||||
end
|
||||
|
||||
decl.type_parameters = names
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def finalize!
|
||||
check_live!
|
||||
|
||||
if decl.returns.equal?(ARG_NOT_PROVIDED)
|
||||
raise BuilderError.new("You must provide a return type; use the `.returns` or `.void` builder methods.")
|
||||
end
|
||||
|
||||
if decl.bind.equal?(ARG_NOT_PROVIDED)
|
||||
decl.bind = nil
|
||||
end
|
||||
if decl.checked.equal?(ARG_NOT_PROVIDED)
|
||||
default_checked_level = T::Private::RuntimeLevels.default_checked_level
|
||||
if (default_checked_level == :never || default_checked_level == :compiled) && !decl.on_failure.equal?(ARG_NOT_PROVIDED)
|
||||
raise BuilderError.new("To use .on_failure you must additionally call .checked(:tests) or .checked(:always), otherwise, the .on_failure has no effect.")
|
||||
end
|
||||
decl.checked = default_checked_level
|
||||
end
|
||||
if decl.on_failure.equal?(ARG_NOT_PROVIDED)
|
||||
decl.on_failure = nil
|
||||
end
|
||||
if decl.params.equal?(ARG_NOT_PROVIDED)
|
||||
decl.params = {}
|
||||
end
|
||||
if decl.type_parameters.equal?(ARG_NOT_PROVIDED)
|
||||
decl.type_parameters = {}
|
||||
end
|
||||
|
||||
decl.finalized = true
|
||||
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Private::Methods::Modes
|
||||
def self.standard
|
||||
'standard'
|
||||
end
|
||||
def self.abstract
|
||||
'abstract'
|
||||
end
|
||||
def self.overridable
|
||||
'overridable'
|
||||
end
|
||||
def self.override
|
||||
'override'
|
||||
end
|
||||
def self.overridable_override
|
||||
'overridable_override'
|
||||
end
|
||||
def self.untyped
|
||||
'untyped'
|
||||
end
|
||||
MODES = [self.standard, self.abstract, self.overridable, self.override, self.overridable_override, self.untyped].freeze
|
||||
|
||||
OVERRIDABLE_MODES = [self.override, self.overridable, self.overridable_override, self.untyped, self.abstract].freeze
|
||||
OVERRIDE_MODES = [self.override, self.overridable_override].freeze
|
||||
NON_OVERRIDE_MODES = MODES - OVERRIDE_MODES
|
||||
end
|
@ -0,0 +1,225 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
class T::Private::Methods::Signature
|
||||
attr_reader :method, :method_name, :arg_types, :kwarg_types, :block_type, :block_name,
|
||||
:rest_type, :rest_name, :keyrest_type, :keyrest_name, :bind,
|
||||
:return_type, :mode, :req_arg_count, :req_kwarg_names, :has_rest, :has_keyrest,
|
||||
:check_level, :parameters, :on_failure, :override_allow_incompatible,
|
||||
:defined_raw
|
||||
|
||||
def self.new_untyped(method:, mode: T::Private::Methods::Modes.untyped, parameters: method.parameters)
|
||||
# Using `Untyped` ensures we'll get an error if we ever try validation on these.
|
||||
not_typed = T::Private::Types::NotTyped.new
|
||||
raw_return_type = not_typed
|
||||
# Map missing parameter names to "argN" positionally
|
||||
parameters = parameters.each_with_index.map do |(param_kind, param_name), index|
|
||||
[param_kind, param_name || "arg#{index}"]
|
||||
end
|
||||
raw_arg_types = parameters.map do |_param_kind, param_name|
|
||||
[param_name, not_typed]
|
||||
end.to_h
|
||||
|
||||
self.new(
|
||||
method: method,
|
||||
method_name: method.name,
|
||||
raw_arg_types: raw_arg_types,
|
||||
raw_return_type: raw_return_type,
|
||||
bind: nil,
|
||||
mode: mode,
|
||||
check_level: :never,
|
||||
parameters: parameters,
|
||||
on_failure: nil,
|
||||
)
|
||||
end
|
||||
|
||||
def initialize(method:, method_name:, raw_arg_types:, raw_return_type:, bind:, mode:, check_level:, on_failure:, parameters: method.parameters, override_allow_incompatible: false, defined_raw: false)
|
||||
@method = method
|
||||
@method_name = method_name
|
||||
@arg_types = []
|
||||
@kwarg_types = {}
|
||||
@block_type = nil
|
||||
@block_name = nil
|
||||
@rest_type = nil
|
||||
@rest_name = nil
|
||||
@keyrest_type = nil
|
||||
@keyrest_name = nil
|
||||
@return_type = T::Utils.coerce(raw_return_type)
|
||||
@bind = bind ? T::Utils.coerce(bind) : bind
|
||||
@mode = mode
|
||||
@check_level = check_level
|
||||
@req_arg_count = 0
|
||||
@req_kwarg_names = []
|
||||
@has_rest = false
|
||||
@has_keyrest = false
|
||||
@parameters = parameters
|
||||
@on_failure = on_failure
|
||||
@override_allow_incompatible = override_allow_incompatible
|
||||
@defined_raw = defined_raw
|
||||
|
||||
declared_param_names = raw_arg_types.keys
|
||||
# If sig params are declared but there is a single parameter with a missing name
|
||||
# **and** the method ends with a "=", assume it is a writer method generated
|
||||
# by attr_writer or attr_accessor
|
||||
writer_method = declared_param_names != [nil] && parameters == [[:req]] && method_name[-1] == "="
|
||||
# For writer methods, map the single parameter to the method name without the "=" at the end
|
||||
parameters = [[:req, method_name[0...-1].to_sym]] if writer_method
|
||||
param_names = parameters.map {|_, name| name}
|
||||
missing_names = param_names - declared_param_names
|
||||
extra_names = declared_param_names - param_names
|
||||
if !missing_names.empty?
|
||||
raise "The declaration for `#{method.name}` is missing parameter(s): #{missing_names.join(', ')}"
|
||||
end
|
||||
if !extra_names.empty?
|
||||
raise "The declaration for `#{method.name}` has extra parameter(s): #{extra_names.join(', ')}"
|
||||
end
|
||||
|
||||
if parameters.size != raw_arg_types.size
|
||||
raise "The declaration for `#{method.name}` has arguments with duplicate names"
|
||||
end
|
||||
|
||||
parameters.zip(raw_arg_types) do |(param_kind, param_name), (type_name, raw_type)|
|
||||
if type_name != param_name
|
||||
hint = ""
|
||||
# Ruby reorders params so that required keyword arguments
|
||||
# always precede optional keyword arguments. We can't tell
|
||||
# whether the culprit is the Ruby reordering or user error, so
|
||||
# we error but include a note
|
||||
if param_kind == :keyreq && parameters.any? {|k, _| k == :key}
|
||||
hint = "\n\nNote: Any required keyword arguments must precede any optional keyword " \
|
||||
"arguments. If your method declaration matches your `def`, try reordering any " \
|
||||
"optional keyword parameters to the end of the method list."
|
||||
end
|
||||
|
||||
raise "Parameter `#{type_name}` is declared out of order (declared as arg number " \
|
||||
"#{declared_param_names.index(type_name) + 1}, defined in the method as arg number " \
|
||||
"#{param_names.index(type_name) + 1}).#{hint}\nMethod: #{method_desc}"
|
||||
end
|
||||
|
||||
type = T::Utils.coerce(raw_type)
|
||||
|
||||
case param_kind
|
||||
when :req
|
||||
if @arg_types.length > @req_arg_count
|
||||
# Note that this is actually is supported by Ruby, but it would add complexity to
|
||||
# support it here, and I'm happy to discourage its use anyway.
|
||||
#
|
||||
# If you are seeing this error and surprised by it, it's possible that you have
|
||||
# overridden the method described in the error message. For example, Rails defines
|
||||
# def self.update!(id = :all, attributes)
|
||||
# on AR models. If you have also defined `self.update!` on an AR model you might
|
||||
# see this error. The simplest resolution is to rename your method.
|
||||
raise "Required params after optional params are not supported in method declarations. Method: #{method_desc}"
|
||||
end
|
||||
@arg_types << [param_name, type]
|
||||
@req_arg_count += 1
|
||||
when :opt
|
||||
@arg_types << [param_name, type]
|
||||
when :key, :keyreq
|
||||
@kwarg_types[param_name] = type
|
||||
if param_kind == :keyreq
|
||||
@req_kwarg_names << param_name
|
||||
end
|
||||
when :block
|
||||
@block_name = param_name
|
||||
@block_type = type
|
||||
when :rest
|
||||
@has_rest = true
|
||||
@rest_name = param_name
|
||||
@rest_type = type
|
||||
when :keyrest
|
||||
@has_keyrest = true
|
||||
@keyrest_name = param_name
|
||||
@keyrest_type = type
|
||||
else
|
||||
raise "Unexpected param_kind: `#{param_kind}`. Method: #{method_desc}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attr_writer :method_name
|
||||
protected :method_name=
|
||||
|
||||
def as_alias(alias_name)
|
||||
new_sig = clone
|
||||
new_sig.method_name = alias_name
|
||||
new_sig
|
||||
end
|
||||
|
||||
def arg_count
|
||||
@arg_types.length
|
||||
end
|
||||
|
||||
def kwarg_names
|
||||
@kwarg_types.keys
|
||||
end
|
||||
|
||||
def owner
|
||||
@method.owner
|
||||
end
|
||||
|
||||
def dsl_method
|
||||
"#{@mode}_method"
|
||||
end
|
||||
|
||||
# @return [Hash] a mapping like `{arg_name: [val, type], ...}`, for only those args actually present.
|
||||
def each_args_value_type(args)
|
||||
# Manually split out args and kwargs based on ruby's behavior. Do not try to implement this by
|
||||
# getting ruby to determine the kwargs for you (e.g., by defining this method to take *args and
|
||||
# **kwargs). That won't work, because ruby's behavior for determining kwargs is dependent on the
|
||||
# the other parameters in the method definition, and our method definition here doesn't (and
|
||||
# can't) match the definition of the method we're validating. In addition, Ruby has a bug that
|
||||
# causes forwarding **kwargs to do the wrong thing: see https://bugs.ruby-lang.org/issues/10708
|
||||
# and https://bugs.ruby-lang.org/issues/11860.
|
||||
args_length = args.length
|
||||
if (args_length > @req_arg_count) && (!@kwarg_types.empty? || @has_keyrest) && args[-1].is_a?(Hash)
|
||||
kwargs = args[-1]
|
||||
args_length -= 1
|
||||
else
|
||||
kwargs = EMPTY_HASH
|
||||
end
|
||||
|
||||
arg_types = @arg_types
|
||||
|
||||
if @has_rest
|
||||
rest_count = args_length - @arg_types.length
|
||||
rest_count = 0 if rest_count.negative?
|
||||
|
||||
arg_types += [[@rest_name, @rest_type]] * rest_count
|
||||
|
||||
elsif (args_length < @req_arg_count) || (args_length > @arg_types.length)
|
||||
expected_str = @req_arg_count.to_s
|
||||
if @arg_types.length != @req_arg_count
|
||||
expected_str += "..#{@arg_types.length}"
|
||||
end
|
||||
raise ArgumentError.new("wrong number of arguments (given #{args_length}, expected #{expected_str})")
|
||||
end
|
||||
|
||||
begin
|
||||
it = 0
|
||||
while it < args_length
|
||||
yield arg_types[it][0], args[it], arg_types[it][1]
|
||||
it += 1
|
||||
end
|
||||
end
|
||||
|
||||
kwargs.each do |name, val|
|
||||
type = @kwarg_types[name]
|
||||
if !type && @has_keyrest
|
||||
type = @keyrest_type
|
||||
end
|
||||
yield name, val, type if type
|
||||
end
|
||||
end
|
||||
|
||||
def method_desc
|
||||
loc = if @method.source_location
|
||||
@method.source_location.join(':')
|
||||
else
|
||||
"<unknown location>"
|
||||
end
|
||||
"#{@method} at #{loc}"
|
||||
end
|
||||
|
||||
EMPTY_HASH = {}.freeze
|
||||
end
|
@ -0,0 +1,225 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Private::Methods::SignatureValidation
|
||||
Methods = T::Private::Methods
|
||||
Modes = Methods::Modes
|
||||
|
||||
def self.validate(signature)
|
||||
if signature.method_name == :initialize && signature.method.owner.is_a?(Class)
|
||||
# Constructors are special. They look like overrides in terms of a super_method existing,
|
||||
# but in practice, you never call them polymorphically. Conceptually, they're standard
|
||||
# methods (this is consistent with how they're treated in other languages, e.g. Java)
|
||||
if signature.mode != Modes.standard
|
||||
raise "`initialize` should not use `.abstract` or `.implementation` or any other inheritance modifiers."
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
super_method = signature.method.super_method
|
||||
|
||||
if super_method && super_method.owner != signature.method.owner
|
||||
Methods.maybe_run_sig_block_for_method(super_method)
|
||||
super_signature = Methods.signature_for_method(super_method)
|
||||
|
||||
# If the super_method has any kwargs we can't build a
|
||||
# Signature for it, so we'll just skip validation in that case.
|
||||
if !super_signature && !super_method.parameters.select {|kind, _| kind == :rest || kind == :kwrest}.empty?
|
||||
nil
|
||||
else
|
||||
# super_signature can be nil when we're overriding a method (perhaps a builtin) that didn't use
|
||||
# one of the method signature helpers. Use an untyped signature so we can still validate
|
||||
# everything but types.
|
||||
#
|
||||
# We treat these signatures as overridable, that way people can use `.override` with
|
||||
# overrides of builtins. In the future we could try to distinguish when the method is a
|
||||
# builtin and treat non-builtins as non-overridable (so you'd be forced to declare them with
|
||||
# `.overridable`).
|
||||
#
|
||||
super_signature ||= Methods::Signature.new_untyped(method: super_method)
|
||||
|
||||
validate_override_mode(signature, super_signature)
|
||||
validate_override_shape(signature, super_signature)
|
||||
validate_override_types(signature, super_signature)
|
||||
end
|
||||
else
|
||||
validate_non_override_mode(signature)
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method def self.pretty_mode(signature)
|
||||
if signature.mode == Modes.overridable_override
|
||||
'.overridable.override'
|
||||
else
|
||||
".#{signature.mode}"
|
||||
end
|
||||
end
|
||||
|
||||
def self.validate_override_mode(signature, super_signature)
|
||||
case signature.mode
|
||||
when *Modes::OVERRIDE_MODES
|
||||
# Peaceful
|
||||
when *Modes::NON_OVERRIDE_MODES
|
||||
if super_signature.mode == Modes.standard
|
||||
# Peaceful
|
||||
elsif super_signature.mode == Modes.abstract
|
||||
raise "You must use `.override` when overriding the abstract method `#{signature.method_name}`.\n" \
|
||||
" Abstract definition: #{method_loc_str(super_signature.method)}\n" \
|
||||
" Implementation definition: #{method_loc_str(signature.method)}\n"
|
||||
elsif super_signature.mode != Modes.untyped
|
||||
raise "You must use `.override` when overriding the existing method `#{signature.method_name}`.\n" \
|
||||
" Parent definition: #{method_loc_str(super_signature.method)}\n" \
|
||||
" Child definition: #{method_loc_str(signature.method)}\n"
|
||||
end
|
||||
else
|
||||
raise "Unexpected mode: #{signature.mode}. Please report this bug at https://github.com/sorbet/sorbet/issues"
|
||||
end
|
||||
end
|
||||
|
||||
def self.validate_non_override_mode(signature)
|
||||
case signature.mode
|
||||
when Modes.override
|
||||
if signature.method_name == :each && signature.method.owner < Enumerable
|
||||
# Enumerable#each is the only method in Sorbet's RBI payload that defines an abstract method.
|
||||
# Enumerable#each does not actually exist at runtime, but it is required to be implemented by
|
||||
# any class which includes Enumerable. We want to declare Enumerable#each as abstract so that
|
||||
# people can call it anything which implements the Enumerable interface, and so that it's a
|
||||
# static error to forget to implement it.
|
||||
#
|
||||
# This is a one-off hack, and we should think carefully before adding more methods here.
|
||||
nil
|
||||
else
|
||||
raise "You marked `#{signature.method_name}` as #{pretty_mode(signature)}, but that method doesn't already exist in this class/module to be overriden.\n" \
|
||||
" Either check for typos and for missing includes or super classes to make the parent method shows up\n" \
|
||||
" ... or remove #{pretty_mode(signature)} here: #{method_loc_str(signature.method)}\n"
|
||||
end
|
||||
when Modes.standard, *Modes::NON_OVERRIDE_MODES
|
||||
# Peaceful
|
||||
nil
|
||||
else
|
||||
raise "Unexpected mode: #{signature.mode}. Please report this bug at https://github.com/sorbet/sorbet/issues"
|
||||
end
|
||||
|
||||
# Given a singleton class, we can check if it belongs to a
|
||||
# module by looking at its superclass; given `module M`,
|
||||
# `M.singleton_class.superclass == Module`, which is not true
|
||||
# for any class.
|
||||
owner = signature.method.owner
|
||||
if (signature.mode == Modes.abstract || Modes::OVERRIDABLE_MODES.include?(signature.mode)) &&
|
||||
owner.singleton_class? && owner.superclass == Module
|
||||
raise "Defining an overridable class method (via #{pretty_mode(signature)}) " \
|
||||
"on a module is not allowed. Class methods on " \
|
||||
"modules do not get inherited and thus cannot be overridden."
|
||||
end
|
||||
end
|
||||
|
||||
def self.validate_override_shape(signature, super_signature)
|
||||
return if signature.override_allow_incompatible
|
||||
return if super_signature.mode == Modes.untyped
|
||||
|
||||
method_name = signature.method_name
|
||||
mode_verb = super_signature.mode == Modes.abstract ? 'implements' : 'overrides'
|
||||
|
||||
if !signature.has_rest && signature.arg_count < super_signature.arg_count
|
||||
raise "Your definition of `#{method_name}` must accept at least #{super_signature.arg_count} " \
|
||||
"positional arguments to be compatible with the method it #{mode_verb}: " \
|
||||
"#{base_override_loc_str(signature, super_signature)}"
|
||||
end
|
||||
|
||||
if !signature.has_rest && super_signature.has_rest
|
||||
raise "Your definition of `#{method_name}` must have `*#{super_signature.rest_name}` " \
|
||||
"to be compatible with the method it #{mode_verb}: " \
|
||||
"#{base_override_loc_str(signature, super_signature)}"
|
||||
end
|
||||
|
||||
if signature.req_arg_count > super_signature.req_arg_count
|
||||
raise "Your definition of `#{method_name}` must have no more than #{super_signature.req_arg_count} " \
|
||||
"required argument(s) to be compatible with the method it #{mode_verb}: " \
|
||||
"#{base_override_loc_str(signature, super_signature)}"
|
||||
end
|
||||
|
||||
if !signature.has_keyrest
|
||||
# O(nm), but n and m are tiny here
|
||||
missing_kwargs = super_signature.kwarg_names - signature.kwarg_names
|
||||
if !missing_kwargs.empty?
|
||||
raise "Your definition of `#{method_name}` is missing these keyword arg(s): #{missing_kwargs} " \
|
||||
"which are defined in the method it #{mode_verb}: " \
|
||||
"#{base_override_loc_str(signature, super_signature)}"
|
||||
end
|
||||
end
|
||||
|
||||
if !signature.has_keyrest && super_signature.has_keyrest
|
||||
raise "Your definition of `#{method_name}` must have `**#{super_signature.keyrest_name}` " \
|
||||
"to be compatible with the method it #{mode_verb}: " \
|
||||
"#{base_override_loc_str(signature, super_signature)}"
|
||||
end
|
||||
|
||||
# O(nm), but n and m are tiny here
|
||||
extra_req_kwargs = signature.req_kwarg_names - super_signature.req_kwarg_names
|
||||
if !extra_req_kwargs.empty?
|
||||
raise "Your definition of `#{method_name}` has extra required keyword arg(s) " \
|
||||
"#{extra_req_kwargs} relative to the method it #{mode_verb}, making it incompatible: " \
|
||||
"#{base_override_loc_str(signature, super_signature)}"
|
||||
end
|
||||
|
||||
if super_signature.block_name && !signature.block_name
|
||||
raise "Your definition of `#{method_name}` must accept a block parameter to be compatible " \
|
||||
"with the method it #{mode_verb}: " \
|
||||
"#{base_override_loc_str(signature, super_signature)}"
|
||||
end
|
||||
end
|
||||
|
||||
def self.validate_override_types(signature, super_signature)
|
||||
return if signature.override_allow_incompatible
|
||||
return if super_signature.mode == Modes.untyped
|
||||
return unless [signature, super_signature].all? do |sig|
|
||||
sig.check_level == :always || sig.check_level == :compiled || (sig.check_level == :tests && T::Private::RuntimeLevels.check_tests?)
|
||||
end
|
||||
mode_noun = super_signature.mode == Modes.abstract ? 'implementation' : 'override'
|
||||
|
||||
# arg types must be contravariant
|
||||
super_signature.arg_types.zip(signature.arg_types).each_with_index do |((_super_name, super_type), (name, type)), index|
|
||||
if !super_type.subtype_of?(type)
|
||||
raise "Incompatible type for arg ##{index + 1} (`#{name}`) in signature for #{mode_noun} of method " \
|
||||
"`#{signature.method_name}`:\n" \
|
||||
"* Base: `#{super_type}` (in #{method_loc_str(super_signature.method)})\n" \
|
||||
"* #{mode_noun.capitalize}: `#{type}` (in #{method_loc_str(signature.method)})\n" \
|
||||
"(The types must be contravariant.)"
|
||||
end
|
||||
end
|
||||
|
||||
# kwarg types must be contravariant
|
||||
super_signature.kwarg_types.each do |name, super_type|
|
||||
type = signature.kwarg_types[name]
|
||||
if !super_type.subtype_of?(type)
|
||||
raise "Incompatible type for arg `#{name}` in signature for #{mode_noun} of method `#{signature.method_name}`:\n" \
|
||||
"* Base: `#{super_type}` (in #{method_loc_str(super_signature.method)})\n" \
|
||||
"* #{mode_noun.capitalize}: `#{type}` (in #{method_loc_str(signature.method)})\n" \
|
||||
"(The types must be contravariant.)"
|
||||
end
|
||||
end
|
||||
|
||||
# return types must be covariant
|
||||
if !signature.return_type.subtype_of?(super_signature.return_type)
|
||||
raise "Incompatible return type in signature for #{mode_noun} of method `#{signature.method_name}`:\n" \
|
||||
"* Base: `#{super_signature.return_type}` (in #{method_loc_str(super_signature.method)})\n" \
|
||||
"* #{mode_noun.capitalize}: `#{signature.return_type}` (in #{method_loc_str(signature.method)})\n" \
|
||||
"(The types must be covariant.)"
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method def self.base_override_loc_str(signature, super_signature)
|
||||
mode_noun = super_signature.mode == Modes.abstract ? 'Implementation' : 'Override'
|
||||
"\n * Base definition: in #{method_loc_str(super_signature.method)}" \
|
||||
"\n * #{mode_noun}: in #{method_loc_str(signature.method)}"
|
||||
end
|
||||
|
||||
private_class_method def self.method_loc_str(method)
|
||||
loc = if method.source_location
|
||||
method.source_location.join(':')
|
||||
else
|
||||
"<unknown location>"
|
||||
end
|
||||
"#{method.owner} at #{loc}"
|
||||
end
|
||||
end
|
@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Private
|
||||
module MixesInClassMethods
|
||||
def included(other)
|
||||
mods = Abstract::Data.get(self, :class_methods_mixins)
|
||||
mods.each {|mod| other.extend(mod)}
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
module Mixins
|
||||
def self.declare_mixes_in_class_methods(mixin, class_methods)
|
||||
if mixin.is_a?(Class)
|
||||
raise "Classes cannot be used as mixins, and so mixes_in_class_methods cannot be used on a Class."
|
||||
end
|
||||
|
||||
if Abstract::Data.key?(mixin, :class_methods_mixins)
|
||||
class_methods = Abstract::Data.get(mixin, :class_methods_mixins) + class_methods
|
||||
end
|
||||
|
||||
mixin.singleton_class.include(MixesInClassMethods)
|
||||
Abstract::Data.set(mixin, :class_methods_mixins, class_methods)
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Private::Retry
|
||||
|
||||
# A special singleton used for static analysis of exceptions.
|
||||
module RETRY
|
||||
freeze
|
||||
end
|
||||
end
|
@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
# Used in `sig.checked(level)` to determine when runtime type checking
|
||||
# is enabled on a method.
|
||||
module T::Private::RuntimeLevels
|
||||
LEVELS = [
|
||||
# Validate every call in every environment
|
||||
:always,
|
||||
# Validate in tests, but not in production
|
||||
:tests,
|
||||
# Don't even validate in tests, b/c too expensive,
|
||||
# or b/c we fully trust the static typing
|
||||
:never,
|
||||
# Validate the sig when the file is using the Sorbet Compiler.
|
||||
# Behaves like :never when interpreted.
|
||||
:compiled,
|
||||
].freeze
|
||||
|
||||
@check_tests = false
|
||||
@wrapped_tests_with_validation = false
|
||||
|
||||
@has_read_default_checked_level = false
|
||||
@default_checked_level = :always
|
||||
|
||||
def self.check_tests?
|
||||
# Assume that this code path means that some `sig.checked(:tests)`
|
||||
# has been wrapped (or not wrapped) already, which is a trapdoor
|
||||
# for toggling `@check_tests`.
|
||||
@wrapped_tests_with_validation = true
|
||||
|
||||
@check_tests
|
||||
end
|
||||
|
||||
def self.enable_checking_in_tests
|
||||
if !@check_tests && @wrapped_tests_with_validation
|
||||
all_checked_tests_sigs = T::Private::Methods.all_checked_tests_sigs
|
||||
locations = all_checked_tests_sigs.map {|sig| sig.method.source_location.join(':')}.join("\n- ")
|
||||
raise "Toggle `:tests`-level runtime type checking earlier. " \
|
||||
"There are already some methods wrapped with `sig.checked(:tests)`:\n" \
|
||||
"- #{locations}"
|
||||
end
|
||||
|
||||
_toggle_checking_tests(true)
|
||||
end
|
||||
|
||||
def self.default_checked_level
|
||||
@has_read_default_checked_level = true
|
||||
@default_checked_level
|
||||
end
|
||||
|
||||
def self.default_checked_level=(default_checked_level)
|
||||
if @has_read_default_checked_level
|
||||
raise "Set the default checked level earlier. There are already some methods whose sig blocks have evaluated which would not be affected by the new default."
|
||||
end
|
||||
@default_checked_level = default_checked_level
|
||||
end
|
||||
|
||||
def self._toggle_checking_tests(checked)
|
||||
@check_tests = checked
|
||||
end
|
||||
end
|
@ -0,0 +1,91 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Private::Sealed
|
||||
module NoInherit
|
||||
def inherited(child)
|
||||
super
|
||||
this_line = Kernel.caller.find {|line| !line.match(/in `inherited'$/)}
|
||||
T::Private::Sealed.validate_inheritance(this_line, self, child, 'inherited')
|
||||
@sorbet_sealed_module_all_subclasses << child
|
||||
end
|
||||
|
||||
def sealed_subclasses
|
||||
@sorbet_sealed_module_all_subclasses_set ||= # rubocop:disable Naming/MemoizedInstanceVariableName
|
||||
begin
|
||||
require 'set'
|
||||
Set.new(@sorbet_sealed_module_all_subclasses).freeze
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module NoIncludeExtend
|
||||
def included(child)
|
||||
super
|
||||
this_line = Kernel.caller.find {|line| !line.match(/in `included'$/)}
|
||||
T::Private::Sealed.validate_inheritance(this_line, self, child, 'included')
|
||||
@sorbet_sealed_module_all_subclasses << child
|
||||
end
|
||||
|
||||
def extended(child)
|
||||
super
|
||||
this_line = Kernel.caller.find {|line| !line.match(/in `extended'$/)}
|
||||
T::Private::Sealed.validate_inheritance(this_line, self, child, 'extended')
|
||||
@sorbet_sealed_module_all_subclasses << child
|
||||
end
|
||||
|
||||
def sealed_subclasses
|
||||
# this will freeze the set so that you can never get into a
|
||||
# state where you use the subclasses list and then something
|
||||
# else will add to it
|
||||
@sorbet_sealed_module_all_subclasses_set ||= # rubocop:disable Naming/MemoizedInstanceVariableName
|
||||
begin
|
||||
require 'set'
|
||||
Set.new(@sorbet_sealed_module_all_subclasses).freeze
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.declare(mod, decl_file)
|
||||
if !mod.is_a?(Module)
|
||||
raise "#{mod} is not a class or module and cannot be declared `sealed!`"
|
||||
end
|
||||
if sealed_module?(mod)
|
||||
raise "#{mod} was already declared `sealed!` and cannot be re-declared `sealed!`"
|
||||
end
|
||||
if T::Private::Final.final_module?(mod)
|
||||
raise "#{mod} was already declared `final!` and cannot be declared `sealed!`"
|
||||
end
|
||||
mod.extend(mod.is_a?(Class) ? NoInherit : NoIncludeExtend)
|
||||
if !decl_file
|
||||
raise "Couldn't determine declaration file for sealed class."
|
||||
end
|
||||
mod.instance_variable_set(:@sorbet_sealed_module_decl_file, decl_file)
|
||||
mod.instance_variable_set(:@sorbet_sealed_module_all_subclasses, [])
|
||||
end
|
||||
|
||||
def self.sealed_module?(mod)
|
||||
mod.instance_variable_defined?(:@sorbet_sealed_module_decl_file)
|
||||
end
|
||||
|
||||
def self.validate_inheritance(this_line, parent, child, verb)
|
||||
this_file = this_line&.split(':')&.first
|
||||
decl_file = parent.instance_variable_get(:@sorbet_sealed_module_decl_file) if sealed_module?(parent)
|
||||
|
||||
if !this_file
|
||||
raise "Could not use backtrace to determine file for #{verb} child #{child}"
|
||||
end
|
||||
if !decl_file
|
||||
raise "#{parent} does not seem to be a sealed module (#{verb} by #{child})"
|
||||
end
|
||||
|
||||
if !this_file.start_with?(decl_file)
|
||||
whitelist = T::Configuration.sealed_violation_whitelist
|
||||
if !whitelist.nil? && whitelist.any? {|pattern| this_file =~ pattern}
|
||||
return
|
||||
end
|
||||
|
||||
raise "#{parent} was declared sealed and can only be #{verb} in #{decl_file}, not #{this_file}"
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
# A placeholder for when an untyped thing must provide a type.
|
||||
# Raises an exception if it is ever used for validation.
|
||||
class T::Private::Types::NotTyped < T::Types::Base
|
||||
ERROR_MESSAGE = "Validation is being done on a `NotTyped`. Please report this bug at https://github.com/sorbet/sorbet/issues"
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"<NOT-TYPED>"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
raise ERROR_MESSAGE
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
raise ERROR_MESSAGE
|
||||
end
|
||||
end
|
@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
# Holds a string. Useful for showing type aliases in error messages
|
||||
class T::Private::Types::StringHolder < T::Types::Base
|
||||
attr_reader :string
|
||||
|
||||
def initialize(string)
|
||||
@string = string
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
string
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
false
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
false
|
||||
end
|
||||
end
|
@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Private::Types
|
||||
# Wraps a proc for a type alias to defer its evaluation.
|
||||
class TypeAlias < T::Types::Base
|
||||
|
||||
def initialize(callable)
|
||||
@callable = callable
|
||||
end
|
||||
|
||||
def aliased_type
|
||||
@aliased_type ||= T::Utils.coerce(@callable.call)
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
aliased_type.name
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def recursively_valid?(obj)
|
||||
aliased_type.recursively_valid?(obj)
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
aliased_type.valid?(obj)
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
# A marking class for when methods return void.
|
||||
# Should never appear in types directly.
|
||||
class T::Private::Types::Void < T::Types::Base
|
||||
ERROR_MESSAGE = "Validation is being done on an `Void`. Please report this bug at https://github.com/sorbet/sorbet/issues"
|
||||
|
||||
# The actual return value of `.void` methods.
|
||||
#
|
||||
# Uses `module VOID` because this gives it a readable name when someone
|
||||
# examines it in Pry or with `#inspect` like:
|
||||
#
|
||||
# T::Private::Types::Void::VOID
|
||||
#
|
||||
module VOID
|
||||
freeze
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"<VOID>"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
raise ERROR_MESSAGE
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
raise ERROR_MESSAGE
|
||||
end
|
||||
end
|
169
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/props/_props.rb
vendored
Normal file
169
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/props/_props.rb
vendored
Normal file
@ -0,0 +1,169 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
# A mixin for defining typed properties (attributes).
|
||||
# To get serialization methods (to/from JSON-style hashes), add T::Props::Serializable.
|
||||
# To get a constructor based on these properties, inherit from T::Struct.
|
||||
module T::Props
|
||||
extend T::Helpers
|
||||
|
||||
#####
|
||||
# CAUTION: This mixin is used in hundreds of classes; we want to keep its surface area as narrow
|
||||
# as possible and avoid polluting (and possibly conflicting with) the classes that use it.
|
||||
#
|
||||
# It currently has *zero* instance methods; let's try to keep it that way.
|
||||
# For ClassMethods (below), try to add things to T::Props::Decorator instead unless you are sure
|
||||
# it needs to be exposed here.
|
||||
#####
|
||||
|
||||
module ClassMethods
|
||||
extend T::Sig
|
||||
extend T::Helpers
|
||||
|
||||
def props
|
||||
decorator.props
|
||||
end
|
||||
def plugins
|
||||
@plugins ||= []
|
||||
end
|
||||
|
||||
def decorator_class
|
||||
Decorator
|
||||
end
|
||||
|
||||
def decorator
|
||||
@decorator ||= decorator_class.new(self)
|
||||
end
|
||||
def reload_decorator!
|
||||
@decorator = decorator_class.new(self)
|
||||
end
|
||||
|
||||
# Define a new property. See {file:README.md} for some concrete
|
||||
# examples.
|
||||
#
|
||||
# Defining a property defines a method with the same name as the
|
||||
# property, that returns the current value, and a `prop=` method
|
||||
# to set its value. Properties will be inherited by subclasses of
|
||||
# a document class.
|
||||
#
|
||||
# @param name [Symbol] The name of this property
|
||||
# @param cls [Class,T::Types::Base] The type of this
|
||||
# property. If the type is itself a `Document` subclass, this
|
||||
# property will be recursively serialized/deserialized.
|
||||
# @param rules [Hash] Options to control this property's behavior.
|
||||
# @option rules [T::Boolean,Symbol] :optional If `true`, this property
|
||||
# is never required to be set before an instance is serialized.
|
||||
# If `:on_load` (default), when this property is missing or nil, a
|
||||
# new model cannot be saved, and an existing model can only be
|
||||
# saved if the property was already missing when it was loaded.
|
||||
# If `false`, when the property is missing/nil after deserialization, it
|
||||
# will be set to the default value (as defined by the `default` or
|
||||
# `factory` option) or will raise if they are not present.
|
||||
# Deprecated: For `Model`s, if `:optional` is set to the special value
|
||||
# `:existing`, the property can be saved as nil even if it was
|
||||
# deserialized with a non-nil value. (Deprecated because there should
|
||||
# never be a need for this behavior; the new behavior of non-optional
|
||||
# properties should be sufficient.)
|
||||
# @option rules [Array] :enum An array of legal values; The
|
||||
# property is required to take on one of those values.
|
||||
# @option rules [T::Boolean] :dont_store If true, this property will
|
||||
# not be saved on the hash resulting from
|
||||
# {T::Props::Serializable#serialize}
|
||||
# @option rules [Object] :ifunset A value to be returned if this
|
||||
# property is requested but has never been set (is set to
|
||||
# `nil`). It is applied at property-access time, and never saved
|
||||
# back onto the object or into the database.
|
||||
#
|
||||
# ``:ifunset`` is considered **DEPRECATED** and should not be used
|
||||
# in new code, in favor of just setting a default value.
|
||||
# @option rules [Model, Symbol, Proc] :foreign A model class that this
|
||||
# property is a reference to. Passing `:foreign` will define a
|
||||
# `:"#{name}_"` method, that will load and return the
|
||||
# corresponding foreign model.
|
||||
#
|
||||
# A symbol can be passed to avoid load-order dependencies; It
|
||||
# will be lazily resolved relative to the enclosing module of the
|
||||
# defining class.
|
||||
#
|
||||
# A callable (proc or method) can be passed to dynamically specify the
|
||||
# foreign model. This will be passed the object instance so that other
|
||||
# properties of the object can be used to determine the relevant model
|
||||
# class. It should return a string/symbol class name or the foreign model
|
||||
# class directly.
|
||||
#
|
||||
# @option rules [Object] :default A default value that will be set
|
||||
# by `#initialize` if none is provided in the initialization
|
||||
# hash. This will not affect objects loaded by {.from_hash}.
|
||||
# @option rules [Proc] :factory A `Proc` that will be called to
|
||||
# generate an initial value for this prop on `#initialize`, if
|
||||
# none is provided.
|
||||
# @option rules [T::Boolean] :immutable If true, this prop cannot be
|
||||
# modified after an instance is created or loaded from a hash.
|
||||
# @option rules [T::Boolean] :override It is an error to redeclare a
|
||||
# `prop` that has already been declared (including on a
|
||||
# superclass), unless `:override` is set to `true`.
|
||||
# @option rules [Symbol, Array] :redaction A redaction directive that may
|
||||
# be passed to Chalk::Tools::RedactionUtils.redact_with_directive to
|
||||
# sanitize this parameter for display. Will define a
|
||||
# `:"#{name}_redacted"` method, which will return the value in sanitized
|
||||
# form.
|
||||
#
|
||||
# @return [void]
|
||||
sig {params(name: Symbol, cls: T.untyped, rules: T.untyped).void}
|
||||
def prop(name, cls, rules={})
|
||||
cls = T::Utils.coerce(cls) if !cls.is_a?(Module)
|
||||
decorator.prop_defined(name, cls, rules)
|
||||
end
|
||||
|
||||
# Validates the value of the specified prop. This method allows the caller to
|
||||
# validate a value for a prop without having to set the data on the instance.
|
||||
# Throws if invalid.
|
||||
#
|
||||
# @param prop [Symbol]
|
||||
# @param val [Object]
|
||||
# @return [void]
|
||||
def validate_prop_value(prop, val)
|
||||
decorator.validate_prop_value(prop, val)
|
||||
end
|
||||
|
||||
# Needs to be documented
|
||||
def plugin(mod)
|
||||
decorator.plugin(mod)
|
||||
end
|
||||
|
||||
# Shorthand helper to define a `prop` with `immutable => true`
|
||||
sig {params(name: Symbol, cls_or_args: T.untyped, args: T::Hash[Symbol, T.untyped]).void}
|
||||
def const(name, cls_or_args, args={})
|
||||
if (cls_or_args.is_a?(Hash) && cls_or_args.key?(:immutable)) || args.key?(:immutable)
|
||||
Kernel.raise ArgumentError.new("Cannot pass 'immutable' argument when using 'const' keyword to define a prop")
|
||||
end
|
||||
|
||||
if cls_or_args.is_a?(Hash)
|
||||
self.prop(name, cls_or_args.merge(immutable: true))
|
||||
else
|
||||
self.prop(name, cls_or_args, args.merge(immutable: true))
|
||||
end
|
||||
end
|
||||
|
||||
def included(child)
|
||||
decorator.model_inherited(child)
|
||||
super
|
||||
end
|
||||
|
||||
def prepended(child)
|
||||
decorator.model_inherited(child)
|
||||
super
|
||||
end
|
||||
|
||||
def extended(child)
|
||||
decorator.model_inherited(child.singleton_class)
|
||||
super
|
||||
end
|
||||
|
||||
def inherited(child)
|
||||
decorator.model_inherited(child)
|
||||
super
|
||||
end
|
||||
end
|
||||
mixes_in_class_methods(ClassMethods)
|
||||
end
|
@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Props::Constructor
|
||||
include T::Props::WeakConstructor
|
||||
end
|
||||
|
||||
module T::Props::Constructor::DecoratorMethods
|
||||
extend T::Sig
|
||||
|
||||
# Set values for all props that have no defaults. Override what `WeakConstructor`
|
||||
# does in order to raise errors on nils instead of ignoring them.
|
||||
#
|
||||
# @return [Integer] A count of props that we successfully initialized (which
|
||||
# we'll use to check for any unrecognized input.)
|
||||
#
|
||||
# checked(:never) - O(runtime object construction)
|
||||
sig {params(instance: T::Props::Constructor, hash: T::Hash[Symbol, T.untyped]).returns(Integer).checked(:never)}
|
||||
def construct_props_without_defaults(instance, hash)
|
||||
# Use `each_pair` rather than `count` because, as of Ruby 2.6, the latter delegates to Enumerator
|
||||
# and therefore allocates for each entry.
|
||||
result = 0
|
||||
props_without_defaults&.each_pair do |p, setter_proc|
|
||||
begin
|
||||
val = hash[p]
|
||||
instance.instance_exec(val, &setter_proc)
|
||||
if val || hash.key?(p)
|
||||
result += 1
|
||||
end
|
||||
rescue TypeError, T::Props::InvalidValueError
|
||||
if !hash.key?(p)
|
||||
raise ArgumentError.new("Missing required prop `#{p}` for class `#{instance.class.name}`")
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
@ -0,0 +1,108 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
module T::Props
|
||||
module CustomType
|
||||
extend T::Sig
|
||||
extend T::Helpers
|
||||
|
||||
abstract!
|
||||
|
||||
include Kernel # for `is_a?`
|
||||
|
||||
# Alias for backwards compatibility
|
||||
sig(:final) do
|
||||
params(
|
||||
value: BasicObject,
|
||||
)
|
||||
.returns(T::Boolean)
|
||||
.checked(:never)
|
||||
end
|
||||
def instance?(value)
|
||||
self.===(value)
|
||||
end
|
||||
|
||||
# Alias for backwards compatibility
|
||||
sig(:final) do
|
||||
params(
|
||||
value: BasicObject,
|
||||
)
|
||||
.returns(T::Boolean)
|
||||
.checked(:never)
|
||||
end
|
||||
def valid?(value)
|
||||
instance?(value)
|
||||
end
|
||||
|
||||
# Given an instance of this type, serialize that into a scalar type
|
||||
# supported by T::Props.
|
||||
#
|
||||
# @param [Object] instance
|
||||
# @return An instance of one of T::Configuration.scalar_types
|
||||
sig {abstract.params(instance: T.untyped).returns(T.untyped).checked(:never)}
|
||||
def serialize(instance); end
|
||||
|
||||
# Given the serialized form of your type, this returns an instance
|
||||
# of that custom type representing that value.
|
||||
#
|
||||
# @param scalar One of T::Configuration.scalar_types
|
||||
# @return Object
|
||||
sig {abstract.params(scalar: T.untyped).returns(T.untyped).checked(:never)}
|
||||
def deserialize(scalar); end
|
||||
|
||||
sig {override.params(_base: Module).void}
|
||||
def self.included(_base)
|
||||
super
|
||||
|
||||
raise 'Please use "extend", not "include" to attach this module'
|
||||
end
|
||||
|
||||
sig(:final) {params(val: Object).returns(T::Boolean).checked(:never)}
|
||||
def self.scalar_type?(val)
|
||||
# We don't need to check for val's included modules in
|
||||
# T::Configuration.scalar_types, because T::Configuration.scalar_types
|
||||
# are all classes.
|
||||
klass = T.let(val.class, T.nilable(Class))
|
||||
until klass.nil?
|
||||
return true if T::Configuration.scalar_types.include?(klass.to_s)
|
||||
klass = klass.superclass
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# We allow custom types to serialize to Arrays, so that we can
|
||||
# implement set-like fields that store a unique-array, but forbid
|
||||
# hashes; Custom hash types should be implemented via an emebdded
|
||||
# T::Struct (or a subclass like Chalk::ODM::Document) or via T.
|
||||
sig(:final) {params(val: Object).returns(T::Boolean).checked(:never)}
|
||||
def self.valid_serialization?(val)
|
||||
case val
|
||||
when Array
|
||||
val.each do |v|
|
||||
return false unless scalar_type?(v)
|
||||
end
|
||||
|
||||
true
|
||||
else
|
||||
scalar_type?(val)
|
||||
end
|
||||
end
|
||||
|
||||
sig(:final) do
|
||||
params(instance: Object)
|
||||
.returns(T.untyped)
|
||||
.checked(:never)
|
||||
end
|
||||
def self.checked_serialize(instance)
|
||||
val = T.cast(instance.class, T::Props::CustomType).serialize(instance)
|
||||
unless valid_serialization?(val)
|
||||
msg = "#{instance.class} did not serialize to a valid scalar type. It became a: #{val.class}"
|
||||
if val.is_a?(Hash)
|
||||
msg += "\nIf you want to store a structured Hash, consider using a T::Struct as your type."
|
||||
end
|
||||
raise TypeError.new(msg)
|
||||
end
|
||||
val
|
||||
end
|
||||
end
|
||||
end
|
669
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/props/decorator.rb
vendored
Normal file
669
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/props/decorator.rb
vendored
Normal file
@ -0,0 +1,669 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
# NB: This is not actually a decorator. It's just named that way for consistency
|
||||
# with DocumentDecorator and ModelDecorator (which both seem to have been written
|
||||
# with an incorrect understanding of the decorator pattern). These "decorators"
|
||||
# should really just be static methods on private modules (we'd also want/need to
|
||||
# replace decorator overrides in plugins with class methods that expose the necessary
|
||||
# functionality).
|
||||
class T::Props::Decorator
|
||||
extend T::Sig
|
||||
|
||||
Rules = T.type_alias {T::Hash[Symbol, T.untyped]}
|
||||
DecoratedInstance = T.type_alias {Object} # Would be T::Props, but that produces circular reference errors in some circumstances
|
||||
PropType = T.type_alias {T::Types::Base}
|
||||
PropTypeOrClass = T.type_alias {T.any(PropType, Module)}
|
||||
|
||||
class NoRulesError < StandardError; end
|
||||
|
||||
EMPTY_PROPS = T.let({}.freeze, T::Hash[Symbol, Rules])
|
||||
private_constant :EMPTY_PROPS
|
||||
|
||||
sig {params(klass: T.untyped).void.checked(:never)}
|
||||
def initialize(klass)
|
||||
@class = T.let(klass, T.all(Module, T::Props::ClassMethods))
|
||||
@class.plugins.each do |mod|
|
||||
T::Props::Plugin::Private.apply_decorator_methods(mod, self)
|
||||
end
|
||||
@props = T.let(EMPTY_PROPS, T::Hash[Symbol, Rules])
|
||||
end
|
||||
|
||||
# checked(:never) - O(prop accesses)
|
||||
sig {returns(T::Hash[Symbol, Rules]).checked(:never)}
|
||||
attr_reader :props
|
||||
|
||||
sig {returns(T::Array[Symbol])}
|
||||
def all_props
|
||||
props.keys
|
||||
end
|
||||
|
||||
# checked(:never) - O(prop accesses)
|
||||
sig {params(prop: T.any(Symbol, String)).returns(Rules).checked(:never)}
|
||||
def prop_rules(prop)
|
||||
props[prop.to_sym] || raise("No such prop: #{prop.inspect}")
|
||||
end
|
||||
|
||||
# checked(:never) - Rules hash is expensive to check
|
||||
sig {params(prop: Symbol, rules: Rules).void.checked(:never)}
|
||||
def add_prop_definition(prop, rules)
|
||||
override = rules.delete(:override)
|
||||
|
||||
if props.include?(prop) && !override
|
||||
raise ArgumentError.new("Attempted to redefine prop #{prop.inspect} that's already defined without specifying :override => true: #{prop_rules(prop)}")
|
||||
elsif !props.include?(prop) && override
|
||||
raise ArgumentError.new("Attempted to override a prop #{prop.inspect} that doesn't already exist")
|
||||
end
|
||||
|
||||
@props = @props.merge(prop => rules.freeze).freeze
|
||||
end
|
||||
|
||||
# Heads up!
|
||||
#
|
||||
# There are already too many ad-hoc options on the prop DSL.
|
||||
#
|
||||
# We have already done a lot of work to remove unnecessary and confusing
|
||||
# options. If you're considering adding a new rule key, please come chat with
|
||||
# the Sorbet team first, as we'd really like to learn more about how to best
|
||||
# solve the problem you're encountering.
|
||||
VALID_RULE_KEYS = T.let(%i[
|
||||
enum
|
||||
foreign
|
||||
ifunset
|
||||
immutable
|
||||
override
|
||||
redaction
|
||||
sensitivity
|
||||
without_accessors
|
||||
clobber_existing_method!
|
||||
extra
|
||||
setter_validate
|
||||
_tnilable
|
||||
].map {|k| [k, true]}.to_h.freeze, T::Hash[Symbol, T::Boolean])
|
||||
private_constant :VALID_RULE_KEYS
|
||||
|
||||
sig {params(key: Symbol).returns(T::Boolean).checked(:never)}
|
||||
def valid_rule_key?(key)
|
||||
!!VALID_RULE_KEYS[key]
|
||||
end
|
||||
|
||||
# checked(:never) - O(prop accesses)
|
||||
sig {returns(T.all(Module, T::Props::ClassMethods)).checked(:never)}
|
||||
def decorated_class
|
||||
@class
|
||||
end
|
||||
|
||||
# Accessors
|
||||
|
||||
# Use this to validate that a value will validate for a given prop. Useful for knowing whether a value can be set on a model without setting it.
|
||||
#
|
||||
# checked(:never) - potentially O(prop accesses) depending on usage pattern
|
||||
sig {params(prop: Symbol, val: T.untyped).void.checked(:never)}
|
||||
def validate_prop_value(prop, val)
|
||||
# We call `setter_proc` here without binding to an instance, so it'll run
|
||||
# `instance_variable_set` if validation passes, but nothing will care.
|
||||
# We only care about the validation.
|
||||
prop_rules(prop).fetch(:setter_proc).call(val)
|
||||
end
|
||||
|
||||
# For performance, don't use named params here.
|
||||
# Passing in rules here is purely a performance optimization.
|
||||
# Unlike the other methods that take rules, this one calls prop_rules for
|
||||
# the default, which raises if the prop doesn't exist (this maintains
|
||||
# preexisting behavior).
|
||||
#
|
||||
# Note this path is NOT used by generated setters on instances,
|
||||
# which are defined using `setter_proc` directly.
|
||||
#
|
||||
# checked(:never) - O(prop accesses)
|
||||
sig do
|
||||
params(
|
||||
instance: DecoratedInstance,
|
||||
prop: Symbol,
|
||||
val: T.untyped,
|
||||
rules: Rules
|
||||
)
|
||||
.void
|
||||
.checked(:never)
|
||||
end
|
||||
def prop_set(instance, prop, val, rules=prop_rules(prop))
|
||||
instance.instance_exec(val, &rules.fetch(:setter_proc))
|
||||
end
|
||||
alias_method :set, :prop_set
|
||||
|
||||
# Only Models have any custom get logic but we need to call this on
|
||||
# non-Models since we don't know at code gen time what we have.
|
||||
sig do
|
||||
params(
|
||||
instance: DecoratedInstance,
|
||||
prop: Symbol,
|
||||
value: T.untyped
|
||||
)
|
||||
.returns(T.untyped)
|
||||
.checked(:never)
|
||||
end
|
||||
def prop_get_logic(instance, prop, value)
|
||||
value
|
||||
end
|
||||
|
||||
# For performance, don't use named params here.
|
||||
# Passing in rules here is purely a performance optimization.
|
||||
#
|
||||
# Note this path is NOT used by generated getters on instances,
|
||||
# unless `ifunset` is used on the prop, or `prop_get` is overridden.
|
||||
#
|
||||
# checked(:never) - O(prop accesses)
|
||||
sig do
|
||||
params(
|
||||
instance: DecoratedInstance,
|
||||
prop: T.any(String, Symbol),
|
||||
rules: Rules
|
||||
)
|
||||
.returns(T.untyped)
|
||||
.checked(:never)
|
||||
end
|
||||
def prop_get(instance, prop, rules=prop_rules(prop))
|
||||
val = instance.instance_variable_get(rules[:accessor_key]) if instance.instance_variable_defined?(rules[:accessor_key])
|
||||
if !val.nil?
|
||||
val
|
||||
elsif (d = rules[:ifunset])
|
||||
T::Props::Utils.deep_clone_object(d)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
instance: DecoratedInstance,
|
||||
prop: T.any(String, Symbol),
|
||||
rules: Rules
|
||||
)
|
||||
.returns(T.untyped)
|
||||
.checked(:never)
|
||||
end
|
||||
def prop_get_if_set(instance, prop, rules=prop_rules(prop))
|
||||
instance.instance_variable_get(rules[:accessor_key]) if instance.instance_variable_defined?(rules[:accessor_key])
|
||||
end
|
||||
alias_method :get, :prop_get_if_set # Alias for backwards compatibility
|
||||
|
||||
# checked(:never) - O(prop accesses)
|
||||
sig do
|
||||
params(
|
||||
instance: DecoratedInstance,
|
||||
prop: Symbol,
|
||||
foreign_class: Module,
|
||||
rules: Rules,
|
||||
opts: T::Hash[Symbol, T.untyped],
|
||||
)
|
||||
.returns(T.untyped)
|
||||
.checked(:never)
|
||||
end
|
||||
def foreign_prop_get(instance, prop, foreign_class, rules=prop_rules(prop), opts={})
|
||||
return if !(value = prop_get(instance, prop, rules))
|
||||
T.unsafe(foreign_class).load(value, {}, opts)
|
||||
end
|
||||
|
||||
# TODO: we should really be checking all the methods on `cls`, not just Object
|
||||
BANNED_METHOD_NAMES = T.let(Object.instance_methods.each_with_object({}) {|x, acc| acc[x] = true}.freeze, T::Hash[Symbol, TrueClass])
|
||||
|
||||
# checked(:never) - Rules hash is expensive to check
|
||||
sig do
|
||||
params(
|
||||
name: Symbol,
|
||||
cls: Module,
|
||||
rules: Rules,
|
||||
type: PropTypeOrClass
|
||||
)
|
||||
.void
|
||||
.checked(:never)
|
||||
end
|
||||
def prop_validate_definition!(name, cls, rules, type)
|
||||
validate_prop_name(name)
|
||||
|
||||
if rules.key?(:pii)
|
||||
raise ArgumentError.new("The 'pii:' option for props has been renamed " \
|
||||
"to 'sensitivity:' (in prop #{@class.name}.#{name})")
|
||||
end
|
||||
|
||||
if rules.keys.any? {|k| !valid_rule_key?(k)}
|
||||
raise ArgumentError.new("At least one invalid prop arg supplied in #{self}: #{rules.keys.inspect}")
|
||||
end
|
||||
|
||||
if !rules[:clobber_existing_method!] && !rules[:without_accessors] && BANNED_METHOD_NAMES.include?(name.to_sym)
|
||||
raise ArgumentError.new(
|
||||
"#{name} can't be used as a prop in #{@class} because a method with " \
|
||||
"that name already exists (defined by #{@class.instance_method(name).owner} " \
|
||||
"at #{@class.instance_method(name).source_location || '<unknown>'}). " \
|
||||
"(If using this name is unavoidable, try `without_accessors: true`.)"
|
||||
)
|
||||
end
|
||||
|
||||
extra = rules[:extra]
|
||||
if !extra.nil? && !extra.is_a?(Hash)
|
||||
raise ArgumentError.new("Extra metadata must be a Hash in prop #{@class.name}.#{name}")
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
SAFE_NAME = T.let(/\A[A-Za-z_][A-Za-z0-9_-]*\z/.freeze, Regexp)
|
||||
|
||||
# Used to validate both prop names and serialized forms
|
||||
sig {params(name: T.any(Symbol, String)).void}
|
||||
private def validate_prop_name(name)
|
||||
if !name.match?(SAFE_NAME)
|
||||
raise ArgumentError.new("Invalid prop name in #{@class.name}: #{name}")
|
||||
end
|
||||
end
|
||||
|
||||
# This converts the type from a T::Type to a regular old ruby class.
|
||||
sig {params(type: T::Types::Base).returns(Module)}
|
||||
private def convert_type_to_class(type)
|
||||
case type
|
||||
when T::Types::TypedArray, T::Types::FixedArray
|
||||
Array
|
||||
when T::Types::TypedHash, T::Types::FixedHash
|
||||
Hash
|
||||
when T::Types::TypedSet
|
||||
Set
|
||||
when T::Types::Union
|
||||
# The below unwraps our T.nilable types for T::Props if we can.
|
||||
# This lets us do things like specify: const T.nilable(String), foreign: Opus::DB::Model::Merchant
|
||||
non_nil_type = T::Utils.unwrap_nilable(type)
|
||||
if non_nil_type
|
||||
convert_type_to_class(non_nil_type)
|
||||
else
|
||||
Object
|
||||
end
|
||||
when T::Types::Simple
|
||||
type.raw_type
|
||||
else
|
||||
# This isn't allowed unless whitelisted_for_underspecification is
|
||||
# true, due to the check in prop_validate_definition
|
||||
Object
|
||||
end
|
||||
end
|
||||
|
||||
# Returns `true` when the type of the prop is nilable, or the field is typed
|
||||
# as `T.untyped`, a `:default` is present in the rules hash, and its value is
|
||||
# `nil`. The latter case is a workaround for explicitly not supporting the use
|
||||
# of `T.nilable(T.untyped)`.
|
||||
#
|
||||
# checked(:never) - Rules hash is expensive to check
|
||||
sig do
|
||||
params(
|
||||
cls: PropTypeOrClass,
|
||||
rules: Rules,
|
||||
)
|
||||
.void
|
||||
.checked(:never)
|
||||
end
|
||||
private def prop_nilable?(cls, rules)
|
||||
T::Utils::Nilable.is_union_with_nilclass(cls) || (cls == T.untyped && rules.key?(:default) && rules[:default].nil?)
|
||||
end
|
||||
|
||||
# checked(:never) - Rules hash is expensive to check
|
||||
sig do
|
||||
params(
|
||||
name: T.any(Symbol, String),
|
||||
cls: PropTypeOrClass,
|
||||
rules: Rules,
|
||||
)
|
||||
.void
|
||||
.checked(:never)
|
||||
end
|
||||
def prop_defined(name, cls, rules={})
|
||||
cls = T::Utils.resolve_alias(cls)
|
||||
|
||||
if prop_nilable?(cls, rules)
|
||||
# :_tnilable is introduced internally for performance purpose so that clients do not need to call
|
||||
# T::Utils::Nilable.is_tnilable(cls) again.
|
||||
# It is strictly internal: clients should always use T::Props::Utils.required_prop?() or
|
||||
# T::Props::Utils.optional_prop?() for checking whether a field is required or optional.
|
||||
rules[:_tnilable] = true
|
||||
end
|
||||
|
||||
name = name.to_sym
|
||||
type = cls
|
||||
if !cls.is_a?(Module)
|
||||
cls = convert_type_to_class(cls)
|
||||
end
|
||||
type_object = smart_coerce(type, enum: rules[:enum])
|
||||
|
||||
prop_validate_definition!(name, cls, rules, type_object)
|
||||
|
||||
# Retrive the possible underlying object with T.nilable.
|
||||
type = T::Utils::Nilable.get_underlying_type(type)
|
||||
|
||||
sensitivity_and_pii = {sensitivity: rules[:sensitivity]}
|
||||
normalize = T::Configuration.normalize_sensitivity_and_pii_handler
|
||||
if normalize
|
||||
sensitivity_and_pii = normalize.call(sensitivity_and_pii)
|
||||
|
||||
# We check for Class so this is only applied on concrete
|
||||
# documents/models; We allow mixins containing props to not
|
||||
# specify their PII nature, as long as every class into which they
|
||||
# are ultimately included does.
|
||||
#
|
||||
if sensitivity_and_pii[:pii] && @class.is_a?(Class) && !T.unsafe(@class).contains_pii?
|
||||
raise ArgumentError.new(
|
||||
'Cannot include a pii prop in a class that declares `contains_no_pii`'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
rules = rules.merge(
|
||||
# TODO: The type of this element is confusing. We should refactor so that
|
||||
# it can be always `type_object` (a PropType) or always `cls` (a Module)
|
||||
type: type,
|
||||
type_object: type_object,
|
||||
accessor_key: "@#{name}".to_sym,
|
||||
sensitivity: sensitivity_and_pii[:sensitivity],
|
||||
pii: sensitivity_and_pii[:pii],
|
||||
# extra arbitrary metadata attached by the code defining this property
|
||||
extra: rules[:extra]&.freeze,
|
||||
)
|
||||
|
||||
validate_not_missing_sensitivity(name, rules)
|
||||
|
||||
# for backcompat (the `:array` key is deprecated but because the name is
|
||||
# so generic it's really hard to be sure it's not being relied on anymore)
|
||||
if type.is_a?(T::Types::TypedArray)
|
||||
inner = T::Utils::Nilable.get_underlying_type(type.type)
|
||||
if inner.is_a?(Module)
|
||||
rules[:array] = inner
|
||||
end
|
||||
end
|
||||
|
||||
rules[:setter_proc] = T::Props::Private::SetterFactory.build_setter_proc(@class, name, rules).freeze
|
||||
|
||||
add_prop_definition(name, rules)
|
||||
|
||||
# NB: using `without_accessors` doesn't make much sense unless you also define some other way to
|
||||
# get at the property (e.g., Chalk::ODM::Document exposes `get` and `set`).
|
||||
define_getter_and_setter(name, rules) unless rules[:without_accessors]
|
||||
|
||||
handle_foreign_option(name, cls, rules, rules[:foreign]) if rules[:foreign]
|
||||
handle_redaction_option(name, rules[:redaction]) if rules[:redaction]
|
||||
end
|
||||
|
||||
# checked(:never) - Rules hash is expensive to check
|
||||
sig {params(name: Symbol, rules: Rules).void.checked(:never)}
|
||||
private def define_getter_and_setter(name, rules)
|
||||
T::Configuration.without_ruby_warnings do
|
||||
if !rules[:immutable]
|
||||
if method(:prop_set).owner != T::Props::Decorator
|
||||
@class.send(:define_method, "#{name}=") do |val|
|
||||
T.unsafe(self.class).decorator.prop_set(self, name, val, rules)
|
||||
end
|
||||
else
|
||||
# Fast path (~4x faster as of Ruby 2.6)
|
||||
@class.send(:define_method, "#{name}=", &rules.fetch(:setter_proc))
|
||||
end
|
||||
end
|
||||
|
||||
if method(:prop_get).owner != T::Props::Decorator || rules.key?(:ifunset)
|
||||
@class.send(:define_method, name) do
|
||||
T.unsafe(self.class).decorator.prop_get(self, name, rules)
|
||||
end
|
||||
else
|
||||
# Fast path (~30x faster as of Ruby 2.6)
|
||||
@class.send(:attr_reader, name) # send is used because `attr_reader` is private in 2.4
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(type: PropTypeOrClass, enum: T.untyped)
|
||||
.returns(T::Types::Base)
|
||||
end
|
||||
private def smart_coerce(type, enum:)
|
||||
# Backwards compatibility for pre-T::Types style
|
||||
type = T::Utils.coerce(type)
|
||||
if enum.nil?
|
||||
type
|
||||
else
|
||||
nonnil_type = T::Utils.unwrap_nilable(type)
|
||||
if nonnil_type
|
||||
T.unsafe(T.nilable(T.all(nonnil_type, T.deprecated_enum(enum))))
|
||||
else
|
||||
T.unsafe(T.all(T.unsafe(type), T.deprecated_enum(enum)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# checked(:never) - Rules hash is expensive to check
|
||||
sig {params(prop_name: Symbol, rules: Rules).void.checked(:never)}
|
||||
private def validate_not_missing_sensitivity(prop_name, rules)
|
||||
if rules[:sensitivity].nil?
|
||||
if rules[:redaction]
|
||||
T::Configuration.hard_assert_handler(
|
||||
"#{@class}##{prop_name} has a 'redaction:' annotation but no " \
|
||||
"'sensitivity:' annotation. This is probably wrong, because if a " \
|
||||
"prop needs redaction then it is probably sensitive. Add a " \
|
||||
"sensitivity annotation like 'sensitivity: Opus::Sensitivity::PII." \
|
||||
"whatever', or explicitly override this check with 'sensitivity: []'."
|
||||
)
|
||||
end
|
||||
# TODO(PRIVACYENG-982) Ideally we'd also check for 'password' and possibly
|
||||
# other terms, but this interacts badly with ProtoDefinedDocument because
|
||||
# the proto syntax currently can't declare "sensitivity: []"
|
||||
if /\bsecret\b/.match?(prop_name)
|
||||
T::Configuration.hard_assert_handler(
|
||||
"#{@class}##{prop_name} has the word 'secret' in its name, but no " \
|
||||
"'sensitivity:' annotation. This is probably wrong, because if a " \
|
||||
"prop is named 'secret' then it is probably sensitive. Add a " \
|
||||
"sensitivity annotation like 'sensitivity: Opus::Sensitivity::NonPII." \
|
||||
"security_token', or explicitly override this check with " \
|
||||
"'sensitivity: []'."
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Create `#{prop_name}_redacted` method
|
||||
sig do
|
||||
params(
|
||||
prop_name: Symbol,
|
||||
redaction: T.untyped,
|
||||
)
|
||||
.void
|
||||
end
|
||||
private def handle_redaction_option(prop_name, redaction)
|
||||
redacted_method = "#{prop_name}_redacted"
|
||||
|
||||
@class.send(:define_method, redacted_method) do
|
||||
value = self.public_send(prop_name)
|
||||
handler = T::Configuration.redaction_handler
|
||||
if !handler
|
||||
raise "Using `redaction:` on a prop requires specifying `T::Configuration.redaction_handler`"
|
||||
end
|
||||
handler.call(value, redaction)
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
option_sym: Symbol,
|
||||
foreign: T.untyped,
|
||||
valid_type_msg: String,
|
||||
)
|
||||
.void
|
||||
end
|
||||
private def validate_foreign_option(option_sym, foreign, valid_type_msg:)
|
||||
if foreign.is_a?(Symbol) || foreign.is_a?(String)
|
||||
raise ArgumentError.new(
|
||||
"Using a symbol/string for `#{option_sym}` is no longer supported. Instead, use a Proc " \
|
||||
"that returns the class, e.g., foreign: -> {Foo}"
|
||||
)
|
||||
end
|
||||
|
||||
if !foreign.is_a?(Proc) && !foreign.is_a?(Array) && !foreign.respond_to?(:load)
|
||||
raise ArgumentError.new("The `#{option_sym}` option must be #{valid_type_msg}")
|
||||
end
|
||||
end
|
||||
|
||||
# checked(:never) - Rules hash is expensive to check
|
||||
sig do
|
||||
params(
|
||||
prop_name: T.any(String, Symbol),
|
||||
rules: Rules,
|
||||
foreign: T.untyped,
|
||||
)
|
||||
.void
|
||||
.checked(:never)
|
||||
end
|
||||
private def define_foreign_method(prop_name, rules, foreign)
|
||||
fk_method = "#{prop_name}_"
|
||||
|
||||
# n.b. there's no clear reason *not* to allow additional options
|
||||
# here, but we're baking in `allow_direct_mutation` since we
|
||||
# *haven't* allowed additional options in the past and want to
|
||||
# default to keeping this interface narrow.
|
||||
@class.send(:define_method, fk_method) do |allow_direct_mutation: nil|
|
||||
foreign = T.let(foreign, T.untyped)
|
||||
if foreign.is_a?(Proc)
|
||||
resolved_foreign = foreign.call
|
||||
if !resolved_foreign.respond_to?(:load)
|
||||
raise ArgumentError.new(
|
||||
"The `foreign` proc for `#{prop_name}` must return a model class. " \
|
||||
"Got `#{resolved_foreign.inspect}` instead."
|
||||
)
|
||||
end
|
||||
# `foreign` is part of the closure state, so this will persist to future invocations
|
||||
# of the method, optimizing it so this only runs on the first invocation.
|
||||
foreign = resolved_foreign
|
||||
end
|
||||
opts = if allow_direct_mutation.nil?
|
||||
{}
|
||||
else
|
||||
{allow_direct_mutation: allow_direct_mutation}
|
||||
end
|
||||
|
||||
T.unsafe(self.class).decorator.foreign_prop_get(self, prop_name, foreign, rules, opts)
|
||||
end
|
||||
|
||||
force_fk_method = "#{fk_method}!"
|
||||
@class.send(:define_method, force_fk_method) do |allow_direct_mutation: nil|
|
||||
loaded_foreign = send(fk_method, allow_direct_mutation: allow_direct_mutation)
|
||||
if !loaded_foreign
|
||||
T::Configuration.hard_assert_handler(
|
||||
'Failed to load foreign model',
|
||||
storytime: {method: force_fk_method, class: self.class}
|
||||
)
|
||||
end
|
||||
loaded_foreign
|
||||
end
|
||||
end
|
||||
|
||||
# checked(:never) - Rules hash is expensive to check
|
||||
sig do
|
||||
params(
|
||||
prop_name: Symbol,
|
||||
prop_cls: Module,
|
||||
rules: Rules,
|
||||
foreign: T.untyped,
|
||||
)
|
||||
.void
|
||||
.checked(:never)
|
||||
end
|
||||
private def handle_foreign_option(prop_name, prop_cls, rules, foreign)
|
||||
validate_foreign_option(
|
||||
:foreign, foreign, valid_type_msg: "a model class or a Proc that returns one"
|
||||
)
|
||||
|
||||
if prop_cls != String
|
||||
raise ArgumentError.new("`foreign` can only be used with a prop type of String")
|
||||
end
|
||||
|
||||
if foreign.is_a?(Array)
|
||||
# We don't support arrays with `foreign` because it's hard to both preserve ordering and
|
||||
# keep them from being lurky performance hits by issuing a bunch of un-batched DB queries.
|
||||
# We could potentially address that by porting over something like AmbiguousIDLoader.
|
||||
raise ArgumentError.new(
|
||||
"Using an array for `foreign` is no longer supported. Instead, please use a union type of " \
|
||||
"token types for the prop type, e.g., T.any(Opus::Autogen::Tokens::FooModelToken, Opus::Autogen::Tokens::BarModelToken)"
|
||||
)
|
||||
end
|
||||
|
||||
unless foreign.is_a?(Proc)
|
||||
T::Configuration.soft_assert_handler(<<~MESSAGE, storytime: {prop: prop_name, value: foreign}, notify: 'jerry')
|
||||
Please use a Proc that returns a model class instead of the model class itself as the argument to `foreign`. In other words:
|
||||
|
||||
instead of `prop :foo, String, foreign: FooModel`
|
||||
use `prop :foo, String, foreign: -> {FooModel}`
|
||||
|
||||
MESSAGE
|
||||
end
|
||||
|
||||
define_foreign_method(prop_name, rules, foreign)
|
||||
end
|
||||
|
||||
# TODO: rename this to props_inherited
|
||||
#
|
||||
# This gets called when a module or class that extends T::Props gets included, extended,
|
||||
# prepended, or inherited.
|
||||
sig {params(child: Module).void.checked(:never)}
|
||||
def model_inherited(child)
|
||||
child.extend(T::Props::ClassMethods)
|
||||
child = T.cast(child, T.all(Module, T::Props::ClassMethods))
|
||||
|
||||
child.plugins.concat(decorated_class.plugins)
|
||||
decorated_class.plugins.each do |mod|
|
||||
# NB: apply_class_methods must not be an instance method on the decorator itself,
|
||||
# otherwise we'd have to call child.decorator here, which would create the decorator
|
||||
# before any `decorator_class` override has a chance to take effect (see the comment below).
|
||||
T::Props::Plugin::Private.apply_class_methods(mod, child)
|
||||
end
|
||||
|
||||
props.each do |name, rules|
|
||||
copied_rules = rules.dup
|
||||
# NB: Calling `child.decorator` here is a timb bomb that's going to give someone a really bad
|
||||
# time. Any class that defines props and also overrides the `decorator_class` method is going
|
||||
# to reach this line before its override take effect, turning it into a no-op.
|
||||
child.decorator.add_prop_definition(name, copied_rules)
|
||||
|
||||
# It's a bit tricky to support `prop_get` hooks added by plugins without
|
||||
# sacrificing the `attr_reader` fast path or clobbering customized getters
|
||||
# defined manually on a child.
|
||||
#
|
||||
# To make this work, we _do_ clobber getters defined on the child, but only if:
|
||||
# (a) it's needed in order to support a `prop_get` hook, and
|
||||
# (b) it's safe because the getter was defined by this file.
|
||||
#
|
||||
unless rules[:without_accessors]
|
||||
if clobber_getter?(child, name)
|
||||
child.send(:define_method, name) do
|
||||
T.unsafe(self.class).decorator.prop_get(self, name, rules)
|
||||
end
|
||||
end
|
||||
|
||||
if !rules[:immutable] && clobber_setter?(child, name)
|
||||
child.send(:define_method, "#{name}=") do |val|
|
||||
T.unsafe(self.class).decorator.prop_set(self, name, val, rules)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sig {params(child: T.all(Module, T::Props::ClassMethods), prop: Symbol).returns(T::Boolean).checked(:never)}
|
||||
private def clobber_getter?(child, prop)
|
||||
!!(child.decorator.method(:prop_get).owner != method(:prop_get).owner &&
|
||||
child.instance_method(prop).source_location&.first == __FILE__)
|
||||
end
|
||||
|
||||
sig {params(child: T.all(Module, T::Props::ClassMethods), prop: Symbol).returns(T::Boolean).checked(:never)}
|
||||
private def clobber_setter?(child, prop)
|
||||
!!(child.decorator.method(:prop_set).owner != method(:prop_set).owner &&
|
||||
child.instance_method("#{prop}=").source_location&.first == __FILE__)
|
||||
end
|
||||
|
||||
sig {params(mod: Module).void.checked(:never)}
|
||||
def plugin(mod)
|
||||
decorated_class.plugins << mod
|
||||
T::Props::Plugin::Private.apply_class_methods(mod, decorated_class)
|
||||
T::Props::Plugin::Private.apply_decorator_methods(mod, self)
|
||||
end
|
||||
end
|
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
module T::Props
|
||||
class Error < StandardError; end
|
||||
class InvalidValueError < Error; end
|
||||
class ImmutableProp < Error; end
|
||||
end
|
@ -0,0 +1,277 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Props
|
||||
# Helper to validate generated code, to mitigate security concerns around
|
||||
# `class_eval`. Not called by default; the expectation is this will be used
|
||||
# in a test iterating over all T::Props::Serializable subclasses.
|
||||
#
|
||||
# We validate the exact expected structure of the generated methods as far
|
||||
# as we can, and then where cloning produces an arbitrarily nested structure,
|
||||
# we just validate a lack of side effects.
|
||||
module GeneratedCodeValidation
|
||||
extend Private::Parse
|
||||
|
||||
class ValidationError < RuntimeError; end
|
||||
|
||||
def self.validate_deserialize(source)
|
||||
parsed = parse(source)
|
||||
|
||||
# def %<name>(hash)
|
||||
# ...
|
||||
# end
|
||||
assert_equal(:def, parsed.type)
|
||||
name, args, body = parsed.children
|
||||
assert_equal(:__t_props_generated_deserialize, name)
|
||||
assert_equal(s(:args, s(:arg, :hash)), args)
|
||||
|
||||
assert_equal(:begin, body.type)
|
||||
init, *prop_clauses, ret = body.children
|
||||
|
||||
# found = %<prop_count>
|
||||
# ...
|
||||
# found
|
||||
assert_equal(:lvasgn, init.type)
|
||||
init_name, init_val = init.children
|
||||
assert_equal(:found, init_name)
|
||||
assert_equal(:int, init_val.type)
|
||||
assert_equal(s(:lvar, :found), ret)
|
||||
|
||||
prop_clauses.each_with_index do |clause, i|
|
||||
if i.even?
|
||||
validate_deserialize_hash_read(clause)
|
||||
else
|
||||
validate_deserialize_ivar_set(clause)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.validate_serialize(source)
|
||||
parsed = parse(source)
|
||||
|
||||
# def %<name>(strict)
|
||||
# ...
|
||||
# end
|
||||
assert_equal(:def, parsed.type)
|
||||
name, args, body = parsed.children
|
||||
assert_equal(:__t_props_generated_serialize, name)
|
||||
assert_equal(s(:args, s(:arg, :strict)), args)
|
||||
|
||||
assert_equal(:begin, body.type)
|
||||
init, *prop_clauses, ret = body.children
|
||||
|
||||
# h = {}
|
||||
# ...
|
||||
# h
|
||||
assert_equal(s(:lvasgn, :h, s(:hash)), init)
|
||||
assert_equal(s(:lvar, :h), ret)
|
||||
|
||||
prop_clauses.each do |clause|
|
||||
validate_serialize_clause(clause)
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method def self.validate_serialize_clause(clause)
|
||||
assert_equal(:if, clause.type)
|
||||
condition, if_body, else_body = clause.children
|
||||
|
||||
# if @%<accessor_key>.nil?
|
||||
assert_equal(:send, condition.type)
|
||||
receiver, method = condition.children
|
||||
assert_equal(:ivar, receiver.type)
|
||||
assert_equal(:nil?, method)
|
||||
|
||||
unless if_body.nil?
|
||||
# required_prop_missing_from_serialize(%<prop>) if strict
|
||||
assert_equal(:if, if_body.type)
|
||||
if_strict_condition, if_strict_body, if_strict_else = if_body.children
|
||||
assert_equal(s(:lvar, :strict), if_strict_condition)
|
||||
assert_equal(:send, if_strict_body.type)
|
||||
on_strict_receiver, on_strict_method, on_strict_arg = if_strict_body.children
|
||||
assert_equal(nil, on_strict_receiver)
|
||||
assert_equal(:required_prop_missing_from_serialize, on_strict_method)
|
||||
assert_equal(:sym, on_strict_arg.type)
|
||||
assert_equal(nil, if_strict_else)
|
||||
end
|
||||
|
||||
# h[%<serialized_form>] = ...
|
||||
assert_equal(:send, else_body.type)
|
||||
receiver, method, h_key, h_val = else_body.children
|
||||
assert_equal(s(:lvar, :h), receiver)
|
||||
assert_equal(:[]=, method)
|
||||
assert_equal(:str, h_key.type)
|
||||
|
||||
validate_lack_of_side_effects(h_val, whitelisted_methods_for_serialize)
|
||||
end
|
||||
|
||||
private_class_method def self.validate_deserialize_hash_read(clause)
|
||||
# val = hash[%<serialized_form>s]
|
||||
|
||||
assert_equal(:lvasgn, clause.type)
|
||||
name, val = clause.children
|
||||
assert_equal(:val, name)
|
||||
assert_equal(:send, val.type)
|
||||
receiver, method, arg = val.children
|
||||
assert_equal(s(:lvar, :hash), receiver)
|
||||
assert_equal(:[], method)
|
||||
assert_equal(:str, arg.type)
|
||||
end
|
||||
|
||||
private_class_method def self.validate_deserialize_ivar_set(clause)
|
||||
# %<accessor_key>s = if val.nil?
|
||||
# found -= 1 unless hash.key?(%<serialized_form>s)
|
||||
# %<nil_handler>s
|
||||
# else
|
||||
# %<serialized_val>s
|
||||
# end
|
||||
|
||||
assert_equal(:ivasgn, clause.type)
|
||||
ivar_name, deser_val = clause.children
|
||||
unless ivar_name.is_a?(Symbol)
|
||||
raise ValidationError.new("Unexpected ivar: #{ivar_name}")
|
||||
end
|
||||
|
||||
assert_equal(:if, deser_val.type)
|
||||
condition, if_body, else_body = deser_val.children
|
||||
assert_equal(s(:send, s(:lvar, :val), :nil?), condition)
|
||||
|
||||
assert_equal(:begin, if_body.type)
|
||||
update_found, handle_nil = if_body.children
|
||||
assert_equal(:if, update_found.type)
|
||||
found_condition, found_if_body, found_else_body = update_found.children
|
||||
assert_equal(:send, found_condition.type)
|
||||
receiver, method, arg = found_condition.children
|
||||
assert_equal(s(:lvar, :hash), receiver)
|
||||
assert_equal(:key?, method)
|
||||
assert_equal(:str, arg.type)
|
||||
assert_equal(nil, found_if_body)
|
||||
assert_equal(s(:op_asgn, s(:lvasgn, :found), :-, s(:int, 1)), found_else_body)
|
||||
|
||||
validate_deserialize_handle_nil(handle_nil)
|
||||
|
||||
if else_body.type == :kwbegin
|
||||
rescue_expression, = else_body.children
|
||||
assert_equal(:rescue, rescue_expression.type)
|
||||
|
||||
try, rescue_body = rescue_expression.children
|
||||
validate_lack_of_side_effects(try, whitelisted_methods_for_deserialize)
|
||||
|
||||
assert_equal(:resbody, rescue_body.type)
|
||||
exceptions, assignment, handler = rescue_body.children
|
||||
assert_equal(:array, exceptions.type)
|
||||
exceptions.children.each {|c| assert_equal(:const, c.type)}
|
||||
assert_equal(:lvasgn, assignment.type)
|
||||
assert_equal([:e], assignment.children)
|
||||
|
||||
deserialization_error, val_return = handler.children
|
||||
|
||||
assert_equal(:send, deserialization_error.type)
|
||||
receiver, method, *args = deserialization_error.children
|
||||
assert_equal(nil, receiver)
|
||||
assert_equal(:raise_deserialization_error, method)
|
||||
args.each {|a| validate_lack_of_side_effects(a, whitelisted_methods_for_deserialize)}
|
||||
|
||||
validate_lack_of_side_effects(val_return, whitelisted_methods_for_deserialize)
|
||||
else
|
||||
validate_lack_of_side_effects(else_body, whitelisted_methods_for_deserialize)
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method def self.validate_deserialize_handle_nil(node)
|
||||
case node.type
|
||||
when :hash, :array, :str, :sym, :int, :float, :true, :false, :nil, :const # rubocop:disable Lint/BooleanSymbol
|
||||
# Primitives and constants are safe
|
||||
when :send
|
||||
receiver, method, arg = node.children
|
||||
if receiver.nil?
|
||||
# required_prop_missing_from_deserialize(%<prop>)
|
||||
assert_equal(:required_prop_missing_from_deserialize, method)
|
||||
assert_equal(:sym, arg.type)
|
||||
elsif receiver == self_class_decorator
|
||||
# self.class.decorator.raise_nil_deserialize_error(%<serialized_form>)
|
||||
assert_equal(:raise_nil_deserialize_error, method)
|
||||
assert_equal(:str, arg.type)
|
||||
elsif method == :default
|
||||
# self.class.decorator.props_with_defaults.fetch(%<prop>).default
|
||||
assert_equal(:send, receiver.type)
|
||||
inner_receiver, inner_method, inner_arg = receiver.children
|
||||
assert_equal(
|
||||
s(:send, self_class_decorator, :props_with_defaults),
|
||||
inner_receiver,
|
||||
)
|
||||
assert_equal(:fetch, inner_method)
|
||||
assert_equal(:sym, inner_arg.type)
|
||||
else
|
||||
raise ValidationError.new("Unexpected receiver in nil handler: #{node.inspect}")
|
||||
end
|
||||
else
|
||||
raise ValidationError.new("Unexpected nil handler: #{node.inspect}")
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method def self.self_class_decorator
|
||||
@self_class_decorator ||= s(:send, s(:send, s(:self), :class), :decorator).freeze
|
||||
end
|
||||
|
||||
private_class_method def self.validate_lack_of_side_effects(node, whitelisted_methods_by_receiver_type)
|
||||
case node.type
|
||||
when :const
|
||||
# This is ok, because we'll have validated what method has been called
|
||||
# if applicable
|
||||
when :hash, :array, :str, :sym, :int, :float, :true, :false, :nil, :self # rubocop:disable Lint/BooleanSymbol
|
||||
# Primitives & self are ok
|
||||
when :lvar, :arg, :ivar
|
||||
# Reading local & instance variables & arguments is ok
|
||||
unless node.children.all? {|c| c.is_a?(Symbol)}
|
||||
raise ValidationError.new("Unexpected child for #{node.type}: #{node.inspect}")
|
||||
end
|
||||
when :args, :mlhs, :block, :begin, :if
|
||||
# Blocks etc are read-only if their contents are read-only
|
||||
node.children.each {|c| validate_lack_of_side_effects(c, whitelisted_methods_by_receiver_type) if c}
|
||||
when :send
|
||||
# Sends are riskier so check a whitelist
|
||||
receiver, method, *args = node.children
|
||||
if receiver
|
||||
if receiver.type == :send
|
||||
key = receiver
|
||||
else
|
||||
key = receiver.type
|
||||
validate_lack_of_side_effects(receiver, whitelisted_methods_by_receiver_type)
|
||||
end
|
||||
|
||||
if !whitelisted_methods_by_receiver_type[key]&.include?(method)
|
||||
raise ValidationError.new("Unexpected method #{method} called on #{receiver.inspect}")
|
||||
end
|
||||
end
|
||||
args.each do |arg|
|
||||
validate_lack_of_side_effects(arg, whitelisted_methods_by_receiver_type)
|
||||
end
|
||||
else
|
||||
raise ValidationError.new("Unexpected node type #{node.type}: #{node.inspect}")
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method def self.assert_equal(expected, actual)
|
||||
if expected != actual
|
||||
raise ValidationError.new("Expected #{expected}, got #{actual}")
|
||||
end
|
||||
end
|
||||
|
||||
# Method calls generated by SerdeTransform
|
||||
private_class_method def self.whitelisted_methods_for_serialize
|
||||
@whitelisted_methods_for_serialize ||= {
|
||||
lvar: %i{dup map transform_values transform_keys each_with_object nil? []= serialize},
|
||||
ivar: %i[dup map transform_values transform_keys each_with_object serialize],
|
||||
const: %i[checked_serialize deep_clone_object],
|
||||
}
|
||||
end
|
||||
|
||||
# Method calls generated by SerdeTransform
|
||||
private_class_method def self.whitelisted_methods_for_deserialize
|
||||
@whitelisted_methods_for_deserialize ||= {
|
||||
lvar: %i{dup map transform_values transform_keys each_with_object nil? []= to_f},
|
||||
const: %i[deserialize from_hash deep_clone_object],
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,140 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Props
|
||||
|
||||
# Helper for generating methods that replace themselves with a specialized
|
||||
# version on first use. The main use case is when we want to generate a
|
||||
# method using the full set of props on a class; we can't do that during
|
||||
# prop definition because we have no way of knowing whether we are defining
|
||||
# the last prop.
|
||||
#
|
||||
# See go/M8yrvzX2 (Stripe-internal) for discussion of security considerations.
|
||||
# In outline, while `class_eval` is a bit scary, we believe that as long as
|
||||
# all inputs are defined in version control (and this is enforced by calling
|
||||
# `disable_lazy_evaluation!` appropriately), risk isn't significantly higher
|
||||
# than with build-time codegen.
|
||||
module HasLazilySpecializedMethods
|
||||
extend T::Sig
|
||||
|
||||
class SourceEvaluationDisabled < RuntimeError
|
||||
def initialize
|
||||
super("Evaluation of lazily-defined methods is disabled")
|
||||
end
|
||||
end
|
||||
|
||||
# Disable any future evaluation of lazily-defined methods.
|
||||
#
|
||||
# This is intended to be called after startup but before interacting with
|
||||
# the outside world, to limit attack surface for our `class_eval` use.
|
||||
#
|
||||
# Note it does _not_ prevent explicit calls to `eagerly_define_lazy_methods!`
|
||||
# from working.
|
||||
sig {void}
|
||||
def self.disable_lazy_evaluation!
|
||||
@lazy_evaluation_disabled ||= true
|
||||
end
|
||||
|
||||
sig {returns(T::Boolean)}
|
||||
def self.lazy_evaluation_enabled?
|
||||
!defined?(@lazy_evaluation_disabled) || !@lazy_evaluation_disabled
|
||||
end
|
||||
|
||||
module DecoratorMethods
|
||||
extend T::Sig
|
||||
|
||||
sig {returns(T::Hash[Symbol, T.proc.returns(String)]).checked(:never)}
|
||||
private def lazily_defined_methods
|
||||
@lazily_defined_methods ||= {}
|
||||
end
|
||||
|
||||
sig {returns(T::Hash[Symbol, T.untyped]).checked(:never)}
|
||||
private def lazily_defined_vm_methods
|
||||
@lazily_defined_vm_methods ||= {}
|
||||
end
|
||||
|
||||
sig {params(name: Symbol).void}
|
||||
private def eval_lazily_defined_method!(name)
|
||||
if !HasLazilySpecializedMethods.lazy_evaluation_enabled?
|
||||
raise SourceEvaluationDisabled.new
|
||||
end
|
||||
|
||||
source = lazily_defined_methods.fetch(name).call
|
||||
|
||||
cls = decorated_class
|
||||
cls.class_eval(source.to_s)
|
||||
cls.send(:private, name)
|
||||
end
|
||||
|
||||
sig {params(name: Symbol).void}
|
||||
private def eval_lazily_defined_vm_method!(name)
|
||||
if !HasLazilySpecializedMethods.lazy_evaluation_enabled?
|
||||
raise SourceEvaluationDisabled.new
|
||||
end
|
||||
|
||||
lazily_defined_vm_methods.fetch(name).call
|
||||
|
||||
cls = decorated_class
|
||||
cls.send(:private, name)
|
||||
end
|
||||
|
||||
sig {params(name: Symbol, blk: T.proc.returns(String)).void}
|
||||
private def enqueue_lazy_method_definition!(name, &blk)
|
||||
lazily_defined_methods[name] = blk
|
||||
|
||||
cls = decorated_class
|
||||
if cls.method_defined?(name)
|
||||
# Ruby does not emit "method redefined" warnings for aliased methods
|
||||
# (more robust than undef_method that would create a small window in which the method doesn't exist)
|
||||
cls.send(:alias_method, name, name)
|
||||
end
|
||||
cls.send(:define_method, name) do |*args|
|
||||
self.class.decorator.send(:eval_lazily_defined_method!, name)
|
||||
send(name, *args)
|
||||
end
|
||||
if cls.respond_to?(:ruby2_keywords, true)
|
||||
cls.send(:ruby2_keywords, name)
|
||||
end
|
||||
cls.send(:private, name)
|
||||
end
|
||||
|
||||
sig {params(name: Symbol, blk: T.untyped).void}
|
||||
private def enqueue_lazy_vm_method_definition!(name, &blk)
|
||||
lazily_defined_vm_methods[name] = blk
|
||||
|
||||
cls = decorated_class
|
||||
cls.send(:define_method, name) do |*args|
|
||||
self.class.decorator.send(:eval_lazily_defined_vm_method!, name)
|
||||
send(name, *args)
|
||||
end
|
||||
if cls.respond_to?(:ruby2_keywords, true)
|
||||
cls.send(:ruby2_keywords, name)
|
||||
end
|
||||
cls.send(:private, name)
|
||||
end
|
||||
|
||||
sig {void}
|
||||
def eagerly_define_lazy_methods!
|
||||
return if lazily_defined_methods.empty?
|
||||
|
||||
source = lazily_defined_methods.values.map(&:call).map(&:to_s).join("\n\n")
|
||||
|
||||
cls = decorated_class
|
||||
cls.class_eval(source)
|
||||
lazily_defined_methods.each_key {|name| cls.send(:private, name)}
|
||||
lazily_defined_methods.clear
|
||||
end
|
||||
|
||||
sig {void}
|
||||
def eagerly_define_lazy_vm_methods!
|
||||
return if lazily_defined_vm_methods.empty?
|
||||
|
||||
lazily_defined_vm_methods.values.map(&:call)
|
||||
|
||||
cls = decorated_class
|
||||
lazily_defined_vm_methods.each_key {|name| cls.send(:private, name)}
|
||||
lazily_defined_vm_methods.clear
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,89 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Props::Optional
|
||||
include T::Props::Plugin
|
||||
end
|
||||
|
||||
##############################################
|
||||
|
||||
# NB: This must stay in the same file where T::Props::Optional is defined due to
|
||||
# T::Props::Decorator#apply_plugin; see https://git.corp.stripe.com/stripe-internal/pay-server/blob/fc7f15593b49875f2d0499ffecfd19798bac05b3/chalk/odm/lib/chalk-odm/document_decorator.rb#L716-L717
|
||||
module T::Props::Optional::DecoratorMethods
|
||||
extend T::Sig
|
||||
|
||||
# Heads up!
|
||||
#
|
||||
# There are already too many ad-hoc options on the prop DSL.
|
||||
#
|
||||
# We have already done a lot of work to remove unnecessary and confusing
|
||||
# options. If you're considering adding a new rule key, please come chat with
|
||||
# the Sorbet team first, as we'd really like to learn more about how to best
|
||||
# solve the problem you're encountering.
|
||||
VALID_RULE_KEYS = {
|
||||
default: true,
|
||||
factory: true,
|
||||
}.freeze
|
||||
private_constant :VALID_RULE_KEYS
|
||||
|
||||
DEFAULT_SETTER_RULE_KEY = :_t_props_private_apply_default
|
||||
private_constant :DEFAULT_SETTER_RULE_KEY
|
||||
|
||||
def valid_rule_key?(key)
|
||||
super || VALID_RULE_KEYS[key]
|
||||
end
|
||||
|
||||
def prop_optional?(prop)
|
||||
prop_rules(prop)[:fully_optional]
|
||||
end
|
||||
|
||||
def compute_derived_rules(rules)
|
||||
rules[:fully_optional] = !T::Props::Utils.need_nil_write_check?(rules)
|
||||
rules[:need_nil_read_check] = T::Props::Utils.need_nil_read_check?(rules)
|
||||
end
|
||||
|
||||
# checked(:never) - O(runtime object construction)
|
||||
sig {returns(T::Hash[Symbol, T::Props::Private::ApplyDefault]).checked(:never)}
|
||||
attr_reader :props_with_defaults
|
||||
|
||||
# checked(:never) - O(runtime object construction)
|
||||
sig {returns(T::Hash[Symbol, T::Props::Private::SetterFactory::SetterProc]).checked(:never)}
|
||||
attr_reader :props_without_defaults
|
||||
|
||||
def add_prop_definition(prop, rules)
|
||||
compute_derived_rules(rules)
|
||||
|
||||
default_setter = T::Props::Private::ApplyDefault.for(decorated_class, rules)
|
||||
if default_setter
|
||||
@props_with_defaults ||= {}
|
||||
@props_with_defaults[prop] = default_setter
|
||||
props_without_defaults&.delete(prop) # Handle potential override
|
||||
|
||||
rules[DEFAULT_SETTER_RULE_KEY] = default_setter
|
||||
else
|
||||
@props_without_defaults ||= {}
|
||||
@props_without_defaults[prop] = rules.fetch(:setter_proc)
|
||||
props_with_defaults&.delete(prop) # Handle potential override
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def prop_validate_definition!(name, cls, rules, type)
|
||||
result = super
|
||||
|
||||
if rules.key?(:default) && rules.key?(:factory)
|
||||
raise ArgumentError.new("Setting both :default and :factory is invalid. See: go/chalk-docs")
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def has_default?(rules)
|
||||
rules.include?(DEFAULT_SETTER_RULE_KEY)
|
||||
end
|
||||
|
||||
def get_default(rules, instance_class)
|
||||
rules[DEFAULT_SETTER_RULE_KEY]&.default
|
||||
end
|
||||
end
|
@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Props::Plugin
|
||||
include T::Props
|
||||
extend T::Helpers
|
||||
|
||||
module ClassMethods
|
||||
def included(child)
|
||||
super
|
||||
child.plugin(self)
|
||||
end
|
||||
end
|
||||
mixes_in_class_methods(ClassMethods)
|
||||
|
||||
module Private
|
||||
# These need to be non-instance methods so we can use them without prematurely creating the
|
||||
# child decorator in `model_inherited` (see comments there for details).
|
||||
#
|
||||
# The dynamic constant access below forces this file to be `typed: false`
|
||||
def self.apply_class_methods(plugin, target)
|
||||
if plugin.const_defined?('ClassMethods')
|
||||
# FIXME: This will break preloading, selective test execution, etc if `mod::ClassMethods`
|
||||
# is ever defined in a separate file from `mod`.
|
||||
target.extend(plugin::ClassMethods)
|
||||
end
|
||||
end
|
||||
|
||||
def self.apply_decorator_methods(plugin, target)
|
||||
if plugin.const_defined?('DecoratorMethods')
|
||||
# FIXME: This will break preloading, selective test execution, etc if `mod::DecoratorMethods`
|
||||
# is ever defined in a separate file from `mod`.
|
||||
target.extend(plugin::DecoratorMethods)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,107 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Props::PrettyPrintable
|
||||
include T::Props::Plugin
|
||||
|
||||
# Return a string representation of this object and all of its props
|
||||
def inspect
|
||||
T.unsafe(T.cast(self, Object).class).decorator.inspect_instance(self)
|
||||
end
|
||||
|
||||
# Override the PP gem with something that's similar, but gives us a hook
|
||||
# to do redaction
|
||||
def pretty_inspect
|
||||
T.unsafe(T.cast(self, Object).class).decorator.inspect_instance(self, multiline: true)
|
||||
end
|
||||
|
||||
module DecoratorMethods
|
||||
extend T::Sig
|
||||
|
||||
sig {params(key: Symbol).returns(T::Boolean).checked(:never)}
|
||||
def valid_rule_key?(key)
|
||||
super || key == :inspect
|
||||
end
|
||||
|
||||
sig do
|
||||
params(instance: T::Props::PrettyPrintable, multiline: T::Boolean, indent: String)
|
||||
.returns(String)
|
||||
end
|
||||
def inspect_instance(instance, multiline: false, indent: ' ')
|
||||
components =
|
||||
inspect_instance_components(
|
||||
instance,
|
||||
multiline: multiline,
|
||||
indent: indent
|
||||
)
|
||||
.reject(&:empty?)
|
||||
|
||||
# Not using #<> here as that makes pry highlight these objects
|
||||
# as if they were all comments, whereas this makes them look
|
||||
# like the structured thing they are.
|
||||
if multiline
|
||||
"#{components[0]}:\n" + T.must(components[1..-1]).join("\n")
|
||||
else
|
||||
"<#{components.join(' ')}>"
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(instance: T::Props::PrettyPrintable, multiline: T::Boolean, indent: String)
|
||||
.returns(T::Array[String])
|
||||
end
|
||||
private def inspect_instance_components(instance, multiline:, indent:)
|
||||
pretty_props = T.unsafe(self).all_props.map do |prop|
|
||||
[prop, inspect_prop_value(instance, prop, multiline: multiline, indent: indent)]
|
||||
end
|
||||
|
||||
joined_props = join_props_with_pretty_values(
|
||||
pretty_props,
|
||||
multiline: multiline,
|
||||
indent: indent
|
||||
)
|
||||
|
||||
[
|
||||
T.unsafe(self).decorated_class.to_s,
|
||||
joined_props,
|
||||
]
|
||||
end
|
||||
|
||||
sig do
|
||||
params(instance: T::Props::PrettyPrintable, prop: Symbol, multiline: T::Boolean, indent: String)
|
||||
.returns(String)
|
||||
.checked(:never)
|
||||
end
|
||||
private def inspect_prop_value(instance, prop, multiline:, indent:)
|
||||
val = T.unsafe(self).get(instance, prop)
|
||||
rules = T.unsafe(self).prop_rules(prop)
|
||||
if (custom_inspect = rules[:inspect])
|
||||
if T::Utils.arity(custom_inspect) == 1
|
||||
custom_inspect.call(val)
|
||||
else
|
||||
custom_inspect.call(val, {multiline: multiline, indent: indent})
|
||||
end
|
||||
elsif rules[:sensitivity] && !rules[:sensitivity].empty? && !val.nil?
|
||||
"<REDACTED #{rules[:sensitivity].join(', ')}>"
|
||||
else
|
||||
val.inspect
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(pretty_kvs: T::Array[[Symbol, String]], multiline: T::Boolean, indent: String)
|
||||
.returns(String)
|
||||
end
|
||||
private def join_props_with_pretty_values(pretty_kvs, multiline:, indent: ' ')
|
||||
pairs = pretty_kvs
|
||||
.sort_by {|k, _v| k.to_s}
|
||||
.map {|k, v| "#{k}=#{v}"}
|
||||
|
||||
if multiline
|
||||
indent + pairs.join("\n#{indent}")
|
||||
else
|
||||
pairs.join(', ')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,170 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
module T::Props
|
||||
module Private
|
||||
class ApplyDefault
|
||||
extend T::Sig
|
||||
extend T::Helpers
|
||||
abstract!
|
||||
|
||||
# checked(:never) - O(object construction x prop count)
|
||||
sig {returns(SetterFactory::SetterProc).checked(:never)}
|
||||
attr_reader :setter_proc
|
||||
|
||||
# checked(:never) - We do this with `T.let` instead
|
||||
sig {params(accessor_key: Symbol, setter_proc: SetterFactory::SetterProc).void.checked(:never)}
|
||||
def initialize(accessor_key, setter_proc)
|
||||
@accessor_key = T.let(accessor_key, Symbol)
|
||||
@setter_proc = T.let(setter_proc, SetterFactory::SetterProc)
|
||||
end
|
||||
|
||||
# checked(:never) - O(object construction x prop count)
|
||||
sig {abstract.returns(T.untyped).checked(:never)}
|
||||
def default; end
|
||||
|
||||
# checked(:never) - O(object construction x prop count)
|
||||
sig {abstract.params(instance: T.all(T::Props::Optional, Object)).void.checked(:never)}
|
||||
def set_default(instance); end
|
||||
|
||||
NO_CLONE_TYPES = T.let([TrueClass, FalseClass, NilClass, Symbol, Numeric, T::Enum].freeze, T::Array[Module])
|
||||
|
||||
# checked(:never) - Rules hash is expensive to check
|
||||
sig {params(cls: Module, rules: T::Hash[Symbol, T.untyped]).returns(T.nilable(ApplyDefault)).checked(:never)}
|
||||
def self.for(cls, rules)
|
||||
accessor_key = rules.fetch(:accessor_key)
|
||||
setter = rules.fetch(:setter_proc)
|
||||
|
||||
if rules.key?(:factory)
|
||||
ApplyDefaultFactory.new(cls, rules.fetch(:factory), accessor_key, setter)
|
||||
elsif rules.key?(:default)
|
||||
default = rules.fetch(:default)
|
||||
case default
|
||||
when *NO_CLONE_TYPES
|
||||
return ApplyPrimitiveDefault.new(default, accessor_key, setter)
|
||||
when String
|
||||
if default.frozen?
|
||||
return ApplyPrimitiveDefault.new(default, accessor_key, setter)
|
||||
end
|
||||
when Array
|
||||
if default.empty? && default.class == Array
|
||||
return ApplyEmptyArrayDefault.new(accessor_key, setter)
|
||||
end
|
||||
when Hash
|
||||
if default.empty? && default.default.nil? && T.unsafe(default).default_proc.nil? && default.class == Hash
|
||||
return ApplyEmptyHashDefault.new(accessor_key, setter)
|
||||
end
|
||||
end
|
||||
|
||||
ApplyComplexDefault.new(default, accessor_key, setter)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ApplyFixedDefault < ApplyDefault
|
||||
abstract!
|
||||
|
||||
# checked(:never) - We do this with `T.let` instead
|
||||
sig {params(default: BasicObject, accessor_key: Symbol, setter_proc: SetterFactory::SetterProc).void.checked(:never)}
|
||||
def initialize(default, accessor_key, setter_proc)
|
||||
# FIXME: Ideally we'd check here that the default is actually a valid
|
||||
# value for this field, but existing code relies on the fact that we don't.
|
||||
#
|
||||
# :(
|
||||
#
|
||||
# setter_proc.call(default)
|
||||
@default = T.let(default, BasicObject)
|
||||
super(accessor_key, setter_proc)
|
||||
end
|
||||
|
||||
# checked(:never) - O(object construction x prop count)
|
||||
sig {override.params(instance: T.all(T::Props::Optional, Object)).void.checked(:never)}
|
||||
def set_default(instance)
|
||||
instance.instance_variable_set(@accessor_key, default)
|
||||
end
|
||||
end
|
||||
|
||||
class ApplyPrimitiveDefault < ApplyFixedDefault
|
||||
# checked(:never) - O(object construction x prop count)
|
||||
sig {override.returns(T.untyped).checked(:never)}
|
||||
attr_reader :default
|
||||
end
|
||||
|
||||
class ApplyComplexDefault < ApplyFixedDefault
|
||||
# checked(:never) - O(object construction x prop count)
|
||||
sig {override.returns(T.untyped).checked(:never)}
|
||||
def default
|
||||
T::Props::Utils.deep_clone_object(@default)
|
||||
end
|
||||
end
|
||||
|
||||
# Special case since it's so common, and a literal `[]` is meaningfully
|
||||
# faster than falling back to ApplyComplexDefault or even calling
|
||||
# `some_empty_array.dup`
|
||||
class ApplyEmptyArrayDefault < ApplyDefault
|
||||
# checked(:never) - O(object construction x prop count)
|
||||
sig {override.params(instance: T.all(T::Props::Optional, Object)).void.checked(:never)}
|
||||
def set_default(instance)
|
||||
instance.instance_variable_set(@accessor_key, [])
|
||||
end
|
||||
|
||||
# checked(:never) - O(object construction x prop count)
|
||||
sig {override.returns(T::Array[T.untyped]).checked(:never)}
|
||||
def default
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Special case since it's so common, and a literal `{}` is meaningfully
|
||||
# faster than falling back to ApplyComplexDefault or even calling
|
||||
# `some_empty_hash.dup`
|
||||
class ApplyEmptyHashDefault < ApplyDefault
|
||||
# checked(:never) - O(object construction x prop count)
|
||||
sig {override.params(instance: T.all(T::Props::Optional, Object)).void.checked(:never)}
|
||||
def set_default(instance)
|
||||
instance.instance_variable_set(@accessor_key, {})
|
||||
end
|
||||
|
||||
# checked(:never) - O(object construction x prop count)
|
||||
sig {override.returns(T::Hash[T.untyped, T.untyped]).checked(:never)}
|
||||
def default
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
class ApplyDefaultFactory < ApplyDefault
|
||||
# checked(:never) - We do this with `T.let` instead
|
||||
sig do
|
||||
params(
|
||||
cls: Module,
|
||||
factory: T.any(Proc, Method),
|
||||
accessor_key: Symbol,
|
||||
setter_proc: SetterFactory::SetterProc,
|
||||
)
|
||||
.void
|
||||
.checked(:never)
|
||||
end
|
||||
def initialize(cls, factory, accessor_key, setter_proc)
|
||||
@class = T.let(cls, Module)
|
||||
@factory = T.let(factory, T.any(Proc, Method))
|
||||
super(accessor_key, setter_proc)
|
||||
end
|
||||
|
||||
# checked(:never) - O(object construction x prop count)
|
||||
sig {override.params(instance: T.all(T::Props::Optional, Object)).void.checked(:never)}
|
||||
def set_default(instance)
|
||||
# Use the actual setter to validate the factory returns a legitimate
|
||||
# value every time
|
||||
instance.instance_exec(default, &@setter_proc)
|
||||
end
|
||||
|
||||
# checked(:never) - O(object construction x prop count)
|
||||
sig {override.returns(T.untyped).checked(:never)}
|
||||
def default
|
||||
@class.class_exec(&@factory)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,160 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
module T::Props
|
||||
module Private
|
||||
|
||||
# Generates a specialized `deserialize` implementation for a subclass of
|
||||
# T::Props::Serializable.
|
||||
#
|
||||
# The basic idea is that we analyze the props and for each prop, generate
|
||||
# the simplest possible logic as a block of Ruby source, so that we don't
|
||||
# pay the cost of supporting types like T:::Hash[CustomType, SubstructType]
|
||||
# when deserializing a simple Integer. Then we join those together,
|
||||
# with a little shared logic to be able to detect when we get input keys
|
||||
# that don't match any prop.
|
||||
module DeserializerGenerator
|
||||
extend T::Sig
|
||||
|
||||
# Generate a method that takes a T::Hash[String, T.untyped] representing
|
||||
# serialized props, sets instance variables for each prop found in the
|
||||
# input, and returns the count of we props set (which we can use to check
|
||||
# for unexpected input keys with minimal effect on the fast path).
|
||||
sig do
|
||||
params(
|
||||
props: T::Hash[Symbol, T::Hash[Symbol, T.untyped]],
|
||||
defaults: T::Hash[Symbol, T::Props::Private::ApplyDefault],
|
||||
)
|
||||
.returns(String)
|
||||
.checked(:never)
|
||||
end
|
||||
def self.generate(props, defaults)
|
||||
stored_props = props.reject {|_, rules| rules[:dont_store]}
|
||||
parts = stored_props.map do |prop, rules|
|
||||
# All of these strings should already be validated (directly or
|
||||
# indirectly) in `validate_prop_name`, so we don't bother with a nice
|
||||
# error message, but we double check here to prevent a refactoring
|
||||
# from introducing a security vulnerability.
|
||||
raise unless T::Props::Decorator::SAFE_NAME.match?(prop.to_s)
|
||||
|
||||
hash_key = rules.fetch(:serialized_form)
|
||||
raise unless T::Props::Decorator::SAFE_NAME.match?(hash_key)
|
||||
|
||||
ivar_name = rules.fetch(:accessor_key).to_s
|
||||
raise unless ivar_name.start_with?('@') && T::Props::Decorator::SAFE_NAME.match?(ivar_name[1..-1])
|
||||
|
||||
transformation = SerdeTransform.generate(
|
||||
T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object)),
|
||||
SerdeTransform::Mode::DESERIALIZE,
|
||||
'val'
|
||||
)
|
||||
transformed_val = if transformation
|
||||
# Rescuing exactly NoMethodError is intended as a temporary hack
|
||||
# to preserve the semantics from before codegen. More generally
|
||||
# we are inconsistent about typechecking on deser and need to decide
|
||||
# our strategy here.
|
||||
<<~RUBY
|
||||
begin
|
||||
#{transformation}
|
||||
rescue NoMethodError => e
|
||||
raise_deserialization_error(
|
||||
#{prop.inspect},
|
||||
val,
|
||||
e,
|
||||
)
|
||||
val
|
||||
end
|
||||
RUBY
|
||||
else
|
||||
'val'
|
||||
end
|
||||
|
||||
nil_handler = generate_nil_handler(
|
||||
prop: prop,
|
||||
serialized_form: hash_key,
|
||||
default: defaults[prop],
|
||||
nilable_type: T::Props::Utils.optional_prop?(rules),
|
||||
raise_on_nil_write: !!rules[:raise_on_nil_write],
|
||||
)
|
||||
|
||||
<<~RUBY
|
||||
val = hash[#{hash_key.inspect}]
|
||||
#{ivar_name} = if val.nil?
|
||||
found -= 1 unless hash.key?(#{hash_key.inspect})
|
||||
#{nil_handler}
|
||||
else
|
||||
#{transformed_val}
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
<<~RUBY
|
||||
def __t_props_generated_deserialize(hash)
|
||||
found = #{stored_props.size}
|
||||
#{parts.join("\n\n")}
|
||||
found
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
# This is very similar to what we do in ApplyDefault, but has a few
|
||||
# key differences that mean we don't just re-use the code:
|
||||
#
|
||||
# 1. Where the logic in construction is that we generate a default
|
||||
# if & only if the prop key isn't present in the input, here we'll
|
||||
# generate a default even to override an explicit nil, but only
|
||||
# if the prop is actually required.
|
||||
# 2. Since we're generating raw Ruby source, we can remove a layer
|
||||
# of indirection for marginally better performance; this seems worth
|
||||
# it for the common cases of literals and empty arrays/hashes.
|
||||
# 3. We need to care about the distinction between `raise_on_nil_write`
|
||||
# and actually non-nilable, where new-instance construction doesn't.
|
||||
#
|
||||
# So we fall back to ApplyDefault only when one of the cases just
|
||||
# mentioned doesn't apply.
|
||||
sig do
|
||||
params(
|
||||
prop: Symbol,
|
||||
serialized_form: String,
|
||||
default: T.nilable(ApplyDefault),
|
||||
nilable_type: T::Boolean,
|
||||
raise_on_nil_write: T::Boolean,
|
||||
)
|
||||
.returns(String)
|
||||
.checked(:never)
|
||||
end
|
||||
private_class_method def self.generate_nil_handler(
|
||||
prop:,
|
||||
serialized_form:,
|
||||
default:,
|
||||
nilable_type:,
|
||||
raise_on_nil_write:
|
||||
)
|
||||
if !nilable_type
|
||||
case default
|
||||
when NilClass
|
||||
"self.class.decorator.raise_nil_deserialize_error(#{serialized_form.inspect})"
|
||||
when ApplyPrimitiveDefault
|
||||
literal = default.default
|
||||
case literal
|
||||
when String, Integer, Symbol, Float, TrueClass, FalseClass, NilClass
|
||||
literal.inspect
|
||||
else
|
||||
"self.class.decorator.props_with_defaults.fetch(#{prop.inspect}).default"
|
||||
end
|
||||
when ApplyEmptyArrayDefault
|
||||
'[]'
|
||||
when ApplyEmptyHashDefault
|
||||
'{}'
|
||||
else
|
||||
"self.class.decorator.props_with_defaults.fetch(#{prop.inspect}).default"
|
||||
end
|
||||
elsif raise_on_nil_write
|
||||
"required_prop_missing_from_deserialize(#{prop.inspect})"
|
||||
else
|
||||
'nil'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Props
|
||||
module Private
|
||||
module Parse
|
||||
def parse(source)
|
||||
@current_ruby ||= require_parser(:CurrentRuby)
|
||||
@current_ruby.parse(source)
|
||||
end
|
||||
|
||||
def s(type, *children)
|
||||
@node ||= require_parser(:AST, :Node)
|
||||
@node.new(type, children)
|
||||
end
|
||||
|
||||
private def require_parser(*constants)
|
||||
# This is an optional dependency for sorbet-runtime in general,
|
||||
# but is required here
|
||||
require 'parser/current'
|
||||
|
||||
# Hack to work around the static checker thinking the constant is
|
||||
# undefined
|
||||
cls = Kernel.const_get(:Parser, true)
|
||||
while (const = constants.shift)
|
||||
cls = cls.const_get(const, false)
|
||||
end
|
||||
cls
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,186 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
module T::Props
|
||||
module Private
|
||||
module SerdeTransform
|
||||
extend T::Sig
|
||||
|
||||
class Serialize; end
|
||||
private_constant :Serialize
|
||||
class Deserialize; end
|
||||
private_constant :Deserialize
|
||||
ModeType = T.type_alias {T.any(Serialize, Deserialize)}
|
||||
private_constant :ModeType
|
||||
|
||||
module Mode
|
||||
SERIALIZE = T.let(Serialize.new.freeze, Serialize)
|
||||
DESERIALIZE = T.let(Deserialize.new.freeze, Deserialize)
|
||||
end
|
||||
|
||||
NO_TRANSFORM_TYPES = T.let(
|
||||
[TrueClass, FalseClass, NilClass, Symbol, String].freeze,
|
||||
T::Array[Module],
|
||||
)
|
||||
private_constant :NO_TRANSFORM_TYPES
|
||||
|
||||
sig do
|
||||
params(
|
||||
type: T::Types::Base,
|
||||
mode: ModeType,
|
||||
varname: String,
|
||||
)
|
||||
.returns(T.nilable(String))
|
||||
.checked(:never)
|
||||
end
|
||||
def self.generate(type, mode, varname)
|
||||
case type
|
||||
when T::Types::TypedArray
|
||||
inner = generate(type.type, mode, 'v')
|
||||
if inner.nil?
|
||||
"#{varname}.dup"
|
||||
else
|
||||
"#{varname}.map {|v| #{inner}}"
|
||||
end
|
||||
when T::Types::TypedSet
|
||||
inner = generate(type.type, mode, 'v')
|
||||
if inner.nil?
|
||||
"#{varname}.dup"
|
||||
else
|
||||
"Set.new(#{varname}) {|v| #{inner}}"
|
||||
end
|
||||
when T::Types::TypedHash
|
||||
keys = generate(type.keys, mode, 'k')
|
||||
values = generate(type.values, mode, 'v')
|
||||
if keys && values
|
||||
"#{varname}.each_with_object({}) {|(k,v),h| h[#{keys}] = #{values}}"
|
||||
elsif keys
|
||||
"#{varname}.transform_keys {|k| #{keys}}"
|
||||
elsif values
|
||||
"#{varname}.transform_values {|v| #{values}}"
|
||||
else
|
||||
"#{varname}.dup"
|
||||
end
|
||||
when T::Types::Simple
|
||||
raw = type.raw_type
|
||||
if NO_TRANSFORM_TYPES.any? {|cls| raw <= cls}
|
||||
nil
|
||||
elsif raw <= Float
|
||||
case mode
|
||||
when Deserialize then "#{varname}.to_f"
|
||||
when Serialize then nil
|
||||
else T.absurd(mode)
|
||||
end
|
||||
elsif raw <= Numeric
|
||||
nil
|
||||
elsif raw < T::Props::Serializable
|
||||
handle_serializable_subtype(varname, raw, mode)
|
||||
elsif raw.singleton_class < T::Props::CustomType
|
||||
handle_custom_type(varname, T.unsafe(raw), mode)
|
||||
elsif T::Configuration.scalar_types.include?(raw.name)
|
||||
# It's a bit of a hack that this is separate from NO_TRANSFORM_TYPES
|
||||
# and doesn't check inheritance (like `T::Props::CustomType.scalar_type?`
|
||||
# does), but it covers the main use case (pay-server's custom `Boolean`
|
||||
# module) without either requiring `T::Configuration.scalar_types` to
|
||||
# accept modules instead of strings (which produces load-order issues
|
||||
# and subtle behavior changes) or eating the performance cost of doing
|
||||
# an inheritance check by manually crawling a class hierarchy and doing
|
||||
# string comparisons.
|
||||
nil
|
||||
else
|
||||
"T::Props::Utils.deep_clone_object(#{varname})"
|
||||
end
|
||||
when T::Types::Union
|
||||
non_nil_type = T::Utils.unwrap_nilable(type)
|
||||
if non_nil_type
|
||||
inner = generate(non_nil_type, mode, varname)
|
||||
if inner.nil?
|
||||
nil
|
||||
else
|
||||
"#{varname}.nil? ? nil : #{inner}"
|
||||
end
|
||||
elsif type.types.all? {|t| generate(t, mode, varname).nil?}
|
||||
# Handle, e.g., T::Boolean
|
||||
nil
|
||||
else
|
||||
# We currently deep_clone_object if the type was T.any(Integer, Float).
|
||||
# When we get better support for union types (maybe this specific
|
||||
# union type, because it would be a replacement for
|
||||
# Chalk::ODM::DeprecatedNumemric), we could opt to special case
|
||||
# this union to have no specific serde transform (the only reason
|
||||
# why Float has a special case is because round tripping through
|
||||
# JSON might normalize Floats to Integers)
|
||||
"T::Props::Utils.deep_clone_object(#{varname})"
|
||||
end
|
||||
when T::Types::Intersection
|
||||
dynamic_fallback = "T::Props::Utils.deep_clone_object(#{varname})"
|
||||
|
||||
# Transformations for any members of the intersection type where we
|
||||
# know what we need to do and did not have to fall back to the
|
||||
# dynamic deep clone method.
|
||||
#
|
||||
# NB: This deliberately does include `nil`, which means we know we
|
||||
# don't need to do any transforming.
|
||||
inner_known = type.types
|
||||
.map {|t| generate(t, mode, varname)}
|
||||
.reject {|t| t == dynamic_fallback}
|
||||
.uniq
|
||||
|
||||
if inner_known.size != 1
|
||||
# If there were no cases where we could tell what we need to do,
|
||||
# e.g. if this is `T.all(SomethingWeird, WhoKnows)`, just use the
|
||||
# dynamic fallback.
|
||||
#
|
||||
# If there were multiple cases and they weren't consistent, e.g.
|
||||
# if this is `T.all(String, T::Array[Integer])`, the type is probably
|
||||
# bogus/uninhabited, but use the dynamic fallback because we still
|
||||
# don't have a better option, and this isn't the place to raise that
|
||||
# error.
|
||||
dynamic_fallback
|
||||
else
|
||||
# This is probably something like `T.all(String, SomeMarker)` or
|
||||
# `T.all(SomeEnum, T.deprecated_enum(SomeEnum::FOO))` and we should
|
||||
# treat it like String or SomeEnum even if we don't know what to do
|
||||
# with the rest of the type.
|
||||
inner_known.first
|
||||
end
|
||||
when T::Types::Enum
|
||||
generate(T::Utils.lift_enum(type), mode, varname)
|
||||
else
|
||||
"T::Props::Utils.deep_clone_object(#{varname})"
|
||||
end
|
||||
end
|
||||
|
||||
sig {params(varname: String, type: Module, mode: ModeType).returns(String).checked(:never)}
|
||||
private_class_method def self.handle_serializable_subtype(varname, type, mode)
|
||||
case mode
|
||||
when Serialize
|
||||
"#{varname}.serialize(strict)"
|
||||
when Deserialize
|
||||
type_name = T.must(module_name(type))
|
||||
"#{type_name}.from_hash(#{varname})"
|
||||
else
|
||||
T.absurd(mode)
|
||||
end
|
||||
end
|
||||
|
||||
sig {params(varname: String, type: Module, mode: ModeType).returns(String).checked(:never)}
|
||||
private_class_method def self.handle_custom_type(varname, type, mode)
|
||||
case mode
|
||||
when Serialize
|
||||
"T::Props::CustomType.checked_serialize(#{varname})"
|
||||
when Deserialize
|
||||
type_name = T.must(module_name(type))
|
||||
"#{type_name}.deserialize(#{varname})"
|
||||
else
|
||||
T.absurd(mode)
|
||||
end
|
||||
end
|
||||
|
||||
sig {params(type: Module).returns(T.nilable(String)).checked(:never)}
|
||||
private_class_method def self.module_name(type)
|
||||
T::Configuration.module_name_mangler.call(type)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,76 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
module T::Props
|
||||
module Private
|
||||
|
||||
# Generates a specialized `serialize` implementation for a subclass of
|
||||
# T::Props::Serializable.
|
||||
#
|
||||
# The basic idea is that we analyze the props and for each prop, generate
|
||||
# the simplest possible logic as a block of Ruby source, so that we don't
|
||||
# pay the cost of supporting types like T:::Hash[CustomType, SubstructType]
|
||||
# when serializing a simple Integer. Then we join those together,
|
||||
# with a little shared logic to be able to detect when we get input keys
|
||||
# that don't match any prop.
|
||||
module SerializerGenerator
|
||||
extend T::Sig
|
||||
|
||||
sig do
|
||||
params(
|
||||
props: T::Hash[Symbol, T::Hash[Symbol, T.untyped]],
|
||||
)
|
||||
.returns(String)
|
||||
.checked(:never)
|
||||
end
|
||||
def self.generate(props)
|
||||
stored_props = props.reject {|_, rules| rules[:dont_store]}
|
||||
parts = stored_props.map do |prop, rules|
|
||||
# All of these strings should already be validated (directly or
|
||||
# indirectly) in `validate_prop_name`, so we don't bother with a nice
|
||||
# error message, but we double check here to prevent a refactoring
|
||||
# from introducing a security vulnerability.
|
||||
raise unless T::Props::Decorator::SAFE_NAME.match?(prop.to_s)
|
||||
|
||||
hash_key = rules.fetch(:serialized_form)
|
||||
raise unless T::Props::Decorator::SAFE_NAME.match?(hash_key)
|
||||
|
||||
ivar_name = rules.fetch(:accessor_key).to_s
|
||||
raise unless ivar_name.start_with?('@') && T::Props::Decorator::SAFE_NAME.match?(ivar_name[1..-1])
|
||||
|
||||
transformed_val = SerdeTransform.generate(
|
||||
T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object)),
|
||||
SerdeTransform::Mode::SERIALIZE,
|
||||
ivar_name
|
||||
) || ivar_name
|
||||
|
||||
nil_asserter =
|
||||
if rules[:fully_optional]
|
||||
''
|
||||
else
|
||||
"required_prop_missing_from_serialize(#{prop.inspect}) if strict"
|
||||
end
|
||||
|
||||
# Don't serialize values that are nil to save space (both the
|
||||
# nil value itself and the field name in the serialized BSON
|
||||
# document)
|
||||
<<~RUBY
|
||||
if #{ivar_name}.nil?
|
||||
#{nil_asserter}
|
||||
else
|
||||
h[#{hash_key.inspect}] = #{transformed_val}
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
<<~RUBY
|
||||
def __t_props_generated_serialize(strict)
|
||||
h = {}
|
||||
#{parts.join("\n\n")}
|
||||
h
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,197 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
module T::Props
|
||||
module Private
|
||||
module SetterFactory
|
||||
extend T::Sig
|
||||
|
||||
SetterProc = T.type_alias {T.proc.params(val: T.untyped).void}
|
||||
ValidateProc = T.type_alias {T.proc.params(prop: Symbol, value: T.untyped).void}
|
||||
|
||||
sig do
|
||||
params(
|
||||
klass: T.all(Module, T::Props::ClassMethods),
|
||||
prop: Symbol,
|
||||
rules: T::Hash[Symbol, T.untyped]
|
||||
)
|
||||
.returns(SetterProc)
|
||||
.checked(:never)
|
||||
end
|
||||
def self.build_setter_proc(klass, prop, rules)
|
||||
# Our nil check works differently than a simple T.nilable for various
|
||||
# reasons (including the `raise_on_nil_write` setting and the existence
|
||||
# of defaults & factories), so unwrap any T.nilable and do a check
|
||||
# manually.
|
||||
non_nil_type = T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object))
|
||||
accessor_key = rules.fetch(:accessor_key)
|
||||
validate = rules[:setter_validate]
|
||||
|
||||
# It seems like a bug that this affects the behavior of setters, but
|
||||
# some existing code relies on this behavior
|
||||
has_explicit_nil_default = rules.key?(:default) && rules.fetch(:default).nil?
|
||||
|
||||
# Use separate methods in order to ensure that we only close over necessary
|
||||
# variables
|
||||
if !T::Props::Utils.need_nil_write_check?(rules) || has_explicit_nil_default
|
||||
if validate.nil? && non_nil_type.is_a?(T::Types::Simple)
|
||||
simple_nilable_proc(prop, accessor_key, non_nil_type.raw_type, klass)
|
||||
else
|
||||
nilable_proc(prop, accessor_key, non_nil_type, klass, validate)
|
||||
end
|
||||
else
|
||||
if validate.nil? && non_nil_type.is_a?(T::Types::Simple)
|
||||
simple_non_nil_proc(prop, accessor_key, non_nil_type.raw_type, klass)
|
||||
else
|
||||
non_nil_proc(prop, accessor_key, non_nil_type, klass, validate)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
prop: Symbol,
|
||||
accessor_key: Symbol,
|
||||
non_nil_type: Module,
|
||||
klass: T.all(Module, T::Props::ClassMethods),
|
||||
)
|
||||
.returns(SetterProc)
|
||||
end
|
||||
private_class_method def self.simple_non_nil_proc(prop, accessor_key, non_nil_type, klass)
|
||||
proc do |val|
|
||||
unless val.is_a?(non_nil_type)
|
||||
T::Props::Private::SetterFactory.raise_pretty_error(
|
||||
klass,
|
||||
prop,
|
||||
T::Utils.coerce(non_nil_type),
|
||||
val,
|
||||
)
|
||||
end
|
||||
instance_variable_set(accessor_key, val)
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
prop: Symbol,
|
||||
accessor_key: Symbol,
|
||||
non_nil_type: T::Types::Base,
|
||||
klass: T.all(Module, T::Props::ClassMethods),
|
||||
validate: T.nilable(ValidateProc)
|
||||
)
|
||||
.returns(SetterProc)
|
||||
end
|
||||
private_class_method def self.non_nil_proc(prop, accessor_key, non_nil_type, klass, validate)
|
||||
proc do |val|
|
||||
# this use of recursively_valid? is intentional: unlike for
|
||||
# methods, we want to make sure data at the 'edge'
|
||||
# (e.g. models that go into databases or structs serialized
|
||||
# from disk) are correct, so we use more thorough runtime
|
||||
# checks there
|
||||
if non_nil_type.recursively_valid?(val)
|
||||
validate&.call(prop, val)
|
||||
else
|
||||
T::Props::Private::SetterFactory.raise_pretty_error(
|
||||
klass,
|
||||
prop,
|
||||
non_nil_type,
|
||||
val,
|
||||
)
|
||||
end
|
||||
instance_variable_set(accessor_key, val)
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
prop: Symbol,
|
||||
accessor_key: Symbol,
|
||||
non_nil_type: Module,
|
||||
klass: T.all(Module, T::Props::ClassMethods),
|
||||
)
|
||||
.returns(SetterProc)
|
||||
end
|
||||
private_class_method def self.simple_nilable_proc(prop, accessor_key, non_nil_type, klass)
|
||||
proc do |val|
|
||||
if val.nil?
|
||||
instance_variable_set(accessor_key, nil)
|
||||
elsif val.is_a?(non_nil_type)
|
||||
instance_variable_set(accessor_key, val)
|
||||
else
|
||||
T::Props::Private::SetterFactory.raise_pretty_error(
|
||||
klass,
|
||||
prop,
|
||||
T::Utils.coerce(non_nil_type),
|
||||
val,
|
||||
)
|
||||
instance_variable_set(accessor_key, val)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
prop: Symbol,
|
||||
accessor_key: Symbol,
|
||||
non_nil_type: T::Types::Base,
|
||||
klass: T.all(Module, T::Props::ClassMethods),
|
||||
validate: T.nilable(ValidateProc),
|
||||
)
|
||||
.returns(SetterProc)
|
||||
end
|
||||
private_class_method def self.nilable_proc(prop, accessor_key, non_nil_type, klass, validate)
|
||||
proc do |val|
|
||||
if val.nil?
|
||||
instance_variable_set(accessor_key, nil)
|
||||
# this use of recursively_valid? is intentional: unlike for
|
||||
# methods, we want to make sure data at the 'edge'
|
||||
# (e.g. models that go into databases or structs serialized
|
||||
# from disk) are correct, so we use more thorough runtime
|
||||
# checks there
|
||||
elsif non_nil_type.recursively_valid?(val)
|
||||
validate&.call(prop, val)
|
||||
instance_variable_set(accessor_key, val)
|
||||
else
|
||||
T::Props::Private::SetterFactory.raise_pretty_error(
|
||||
klass,
|
||||
prop,
|
||||
non_nil_type,
|
||||
val,
|
||||
)
|
||||
instance_variable_set(accessor_key, val)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
klass: T.all(Module, T::Props::ClassMethods),
|
||||
prop: Symbol,
|
||||
type: T.any(T::Types::Base, Module),
|
||||
val: T.untyped,
|
||||
)
|
||||
.void
|
||||
end
|
||||
def self.raise_pretty_error(klass, prop, type, val)
|
||||
base_message = "Can't set #{klass.name}.#{prop} to #{val.inspect} (instance of #{val.class}) - need a #{type}"
|
||||
|
||||
pretty_message = "Parameter '#{prop}': #{base_message}\n"
|
||||
caller_loc = caller_locations&.find {|l| !l.to_s.include?('sorbet-runtime/lib/types/props')}
|
||||
if caller_loc
|
||||
pretty_message += "Caller: #{caller_loc.path}:#{caller_loc.lineno}\n"
|
||||
end
|
||||
|
||||
T::Configuration.call_validation_error_handler(
|
||||
nil,
|
||||
message: base_message,
|
||||
pretty_message: pretty_message,
|
||||
kind: 'Parameter',
|
||||
name: prop,
|
||||
type: type,
|
||||
value: val,
|
||||
location: caller_loc,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,374 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Props::Serializable
|
||||
include T::Props::Plugin
|
||||
# Required because we have special handling for `optional: false`
|
||||
include T::Props::Optional
|
||||
# Required because we have special handling for extra_props
|
||||
include T::Props::PrettyPrintable
|
||||
|
||||
# Serializes this object to a hash, suitable for conversion to
|
||||
# JSON/BSON.
|
||||
#
|
||||
# @param strict [T::Boolean] (true) If false, do not raise an
|
||||
# exception if this object has mandatory props with missing
|
||||
# values.
|
||||
# @return [Hash] A serialization of this object.
|
||||
def serialize(strict=true)
|
||||
begin
|
||||
h = __t_props_generated_serialize(strict)
|
||||
rescue => e
|
||||
msg = self.class.decorator.message_with_generated_source_context(
|
||||
e,
|
||||
:__t_props_generated_serialize,
|
||||
:generate_serialize_source
|
||||
)
|
||||
if msg
|
||||
begin
|
||||
raise e.class.new(msg)
|
||||
rescue ArgumentError
|
||||
raise TypeError.new(msg)
|
||||
end
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
h.merge!(@_extra_props) if defined?(@_extra_props)
|
||||
h
|
||||
end
|
||||
|
||||
private def __t_props_generated_serialize(strict)
|
||||
# No-op; will be overridden if there are any props.
|
||||
#
|
||||
# To see the definition for class `Foo`, run `Foo.decorator.send(:generate_serialize_source)`
|
||||
{}
|
||||
end
|
||||
|
||||
# Populates the property values on this object with the values
|
||||
# from a hash. In general, prefer to use {.from_hash} to construct
|
||||
# a new instance, instead of loading into an existing instance.
|
||||
#
|
||||
# @param hash [Hash<String, Object>] The hash to take property
|
||||
# values from.
|
||||
# @param strict [T::Boolean] (false) If true, raise an exception if
|
||||
# the hash contains keys that do not correspond to any known
|
||||
# props on this instance.
|
||||
# @return [void]
|
||||
def deserialize(hash, strict=false)
|
||||
begin
|
||||
hash_keys_matching_props = __t_props_generated_deserialize(hash)
|
||||
rescue => e
|
||||
msg = self.class.decorator.message_with_generated_source_context(
|
||||
e,
|
||||
:__t_props_generated_deserialize,
|
||||
:generate_deserialize_source
|
||||
)
|
||||
if msg
|
||||
begin
|
||||
raise e.class.new(msg)
|
||||
rescue ArgumentError
|
||||
raise TypeError.new(msg)
|
||||
end
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
if hash.size > hash_keys_matching_props
|
||||
serialized_forms = self.class.decorator.prop_by_serialized_forms
|
||||
extra = hash.reject {|k, _| serialized_forms.key?(k)}
|
||||
|
||||
# `extra` could still be empty here if the input matches a `dont_store` prop;
|
||||
# historically, we just ignore those
|
||||
if !extra.empty?
|
||||
if strict
|
||||
raise "Unknown properties for #{self.class.name}: #{extra.keys.inspect}"
|
||||
else
|
||||
@_extra_props = extra
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private def __t_props_generated_deserialize(hash)
|
||||
# No-op; will be overridden if there are any props.
|
||||
#
|
||||
# To see the definition for class `Foo`, run `Foo.decorator.send(:generate_deserialize_source)`
|
||||
0
|
||||
end
|
||||
|
||||
# with() will clone the old object to the new object and merge the specified props to the new object.
|
||||
def with(changed_props)
|
||||
with_existing_hash(changed_props, existing_hash: self.serialize)
|
||||
end
|
||||
|
||||
private def recursive_stringify_keys(obj)
|
||||
if obj.is_a?(Hash)
|
||||
new_obj = obj.class.new
|
||||
obj.each do |k, v|
|
||||
new_obj[k.to_s] = recursive_stringify_keys(v)
|
||||
end
|
||||
elsif obj.is_a?(Array)
|
||||
new_obj = obj.map {|v| recursive_stringify_keys(v)}
|
||||
else
|
||||
new_obj = obj
|
||||
end
|
||||
new_obj
|
||||
end
|
||||
|
||||
private def with_existing_hash(changed_props, existing_hash:)
|
||||
serialized = existing_hash
|
||||
new_val = self.class.from_hash(serialized.merge(recursive_stringify_keys(changed_props)))
|
||||
old_extra = self.instance_variable_get(:@_extra_props) if self.instance_variable_defined?(:@_extra_props)
|
||||
new_extra = new_val.instance_variable_get(:@_extra_props) if new_val.instance_variable_defined?(:@_extra_props)
|
||||
if old_extra != new_extra
|
||||
difference =
|
||||
if old_extra
|
||||
new_extra.reject {|k, v| old_extra[k] == v}
|
||||
else
|
||||
new_extra
|
||||
end
|
||||
raise ArgumentError.new("Unexpected arguments: input(#{changed_props}), unexpected(#{difference})")
|
||||
end
|
||||
new_val
|
||||
end
|
||||
|
||||
# Asserts if this property is missing during strict serialize
|
||||
private def required_prop_missing_from_serialize(prop)
|
||||
if defined?(@_required_props_missing_from_deserialize) &&
|
||||
@_required_props_missing_from_deserialize&.include?(prop)
|
||||
# If the prop was already missing during deserialization, that means the application
|
||||
# code already had to deal with a nil value, which means we wouldn't be accomplishing
|
||||
# much by raising here (other than causing an unnecessary breakage).
|
||||
T::Configuration.log_info_handler(
|
||||
"chalk-odm: missing required property in serialize",
|
||||
prop: prop, class: self.class.name, id: self.class.decorator.get_id(self)
|
||||
)
|
||||
else
|
||||
raise TypeError.new("#{self.class.name}.#{prop} not set for non-optional prop")
|
||||
end
|
||||
end
|
||||
|
||||
# Marks this property as missing during deserialize
|
||||
private def required_prop_missing_from_deserialize(prop)
|
||||
@_required_props_missing_from_deserialize ||= Set[]
|
||||
@_required_props_missing_from_deserialize << prop
|
||||
nil
|
||||
end
|
||||
|
||||
private def raise_deserialization_error(prop_name, value, orig_error)
|
||||
T::Configuration.soft_assert_handler(
|
||||
'Deserialization error (probably unexpected stored type)',
|
||||
storytime: {
|
||||
klass: self.class,
|
||||
prop: prop_name,
|
||||
value: value,
|
||||
error: orig_error.message,
|
||||
notify: 'djudd'
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
##############################################
|
||||
|
||||
# NB: This must stay in the same file where T::Props::Serializable is defined due to
|
||||
# T::Props::Decorator#apply_plugin; see https://git.corp.stripe.com/stripe-internal/pay-server/blob/fc7f15593b49875f2d0499ffecfd19798bac05b3/chalk/odm/lib/chalk-odm/document_decorator.rb#L716-L717
|
||||
module T::Props::Serializable::DecoratorMethods
|
||||
include T::Props::HasLazilySpecializedMethods::DecoratorMethods
|
||||
|
||||
# Heads up!
|
||||
#
|
||||
# There are already too many ad-hoc options on the prop DSL.
|
||||
#
|
||||
# We have already done a lot of work to remove unnecessary and confusing
|
||||
# options. If you're considering adding a new rule key, please come chat with
|
||||
# the Sorbet team first, as we'd really like to learn more about how to best
|
||||
# solve the problem you're encountering.
|
||||
VALID_RULE_KEYS = {dont_store: true, name: true, raise_on_nil_write: true}.freeze
|
||||
private_constant :VALID_RULE_KEYS
|
||||
|
||||
def valid_rule_key?(key)
|
||||
super || VALID_RULE_KEYS[key]
|
||||
end
|
||||
|
||||
def required_props
|
||||
@class.props.select {|_, v| T::Props::Utils.required_prop?(v)}.keys
|
||||
end
|
||||
|
||||
def prop_dont_store?(prop)
|
||||
prop_rules(prop)[:dont_store]
|
||||
end
|
||||
def prop_by_serialized_forms
|
||||
@class.prop_by_serialized_forms
|
||||
end
|
||||
|
||||
def from_hash(hash, strict=false)
|
||||
raise ArgumentError.new("#{hash.inspect} provided to from_hash") if !(hash && hash.is_a?(Hash))
|
||||
|
||||
i = @class.allocate
|
||||
i.deserialize(hash, strict)
|
||||
|
||||
i
|
||||
end
|
||||
|
||||
def prop_serialized_form(prop)
|
||||
prop_rules(prop)[:serialized_form]
|
||||
end
|
||||
|
||||
def serialized_form_prop(serialized_form)
|
||||
prop_by_serialized_forms[serialized_form.to_s] || raise("No such serialized form: #{serialized_form.inspect}")
|
||||
end
|
||||
|
||||
def add_prop_definition(prop, rules)
|
||||
rules[:serialized_form] = rules.fetch(:name, prop.to_s)
|
||||
res = super
|
||||
prop_by_serialized_forms[rules[:serialized_form]] = prop
|
||||
if T::Configuration.use_vm_prop_serde?
|
||||
enqueue_lazy_vm_method_definition!(:__t_props_generated_serialize) {generate_serialize2}
|
||||
enqueue_lazy_vm_method_definition!(:__t_props_generated_deserialize) {generate_deserialize2}
|
||||
else
|
||||
enqueue_lazy_method_definition!(:__t_props_generated_serialize) {generate_serialize_source}
|
||||
enqueue_lazy_method_definition!(:__t_props_generated_deserialize) {generate_deserialize_source}
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
private def generate_serialize_source
|
||||
T::Props::Private::SerializerGenerator.generate(props)
|
||||
end
|
||||
|
||||
private def generate_deserialize_source
|
||||
T::Props::Private::DeserializerGenerator.generate(
|
||||
props,
|
||||
props_with_defaults || {},
|
||||
)
|
||||
end
|
||||
|
||||
private def generate_serialize2
|
||||
T::Props::Private::SerializerGenerator.generate2(decorated_class, props)
|
||||
end
|
||||
|
||||
private def generate_deserialize2
|
||||
T::Props::Private::DeserializerGenerator.generate2(
|
||||
decorated_class,
|
||||
props,
|
||||
props_with_defaults || {},
|
||||
)
|
||||
end
|
||||
|
||||
def message_with_generated_source_context(error, generated_method, generate_source_method)
|
||||
line_label = error.backtrace.find {|l| l.end_with?("in `#{generated_method}'")}
|
||||
return unless line_label
|
||||
|
||||
line_num = line_label.split(':')[1]&.to_i
|
||||
return unless line_num
|
||||
|
||||
source_lines = self.send(generate_source_method).split("\n")
|
||||
previous_blank = source_lines[0...line_num].rindex(&:empty?) || 0
|
||||
next_blank = line_num + (source_lines[line_num..-1]&.find_index(&:empty?) || 0)
|
||||
context = " #{source_lines[(previous_blank + 1)...next_blank].join("\n ")}"
|
||||
<<~MSG
|
||||
Error in #{decorated_class.name}##{generated_method}: #{error.message}
|
||||
at line #{line_num - previous_blank - 1} in:
|
||||
#{context}
|
||||
MSG
|
||||
end
|
||||
|
||||
def raise_nil_deserialize_error(hkey)
|
||||
msg = "Tried to deserialize a required prop from a nil value. It's "\
|
||||
"possible that a nil value exists in the database, so you should "\
|
||||
"provide a `default: or factory:` for this prop (see go/optional "\
|
||||
"for more details). If this is already the case, you probably "\
|
||||
"omitted a required prop from the `fields:` option when doing a "\
|
||||
"partial load."
|
||||
storytime = {prop: hkey, klass: decorated_class.name}
|
||||
|
||||
# Notify the model owner if it exists, and always notify the API owner.
|
||||
begin
|
||||
if T::Configuration.class_owner_finder && (owner = T::Configuration.class_owner_finder.call(decorated_class))
|
||||
T::Configuration.hard_assert_handler(
|
||||
msg,
|
||||
storytime: storytime,
|
||||
project: owner
|
||||
)
|
||||
end
|
||||
ensure
|
||||
T::Configuration.hard_assert_handler(msg, storytime: storytime)
|
||||
end
|
||||
end
|
||||
|
||||
def prop_validate_definition!(name, cls, rules, type)
|
||||
result = super
|
||||
|
||||
if (rules_name = rules[:name])
|
||||
unless rules_name.is_a?(String)
|
||||
raise ArgumentError.new("Invalid name in prop #{@class.name}.#{name}: #{rules_name.inspect}")
|
||||
end
|
||||
|
||||
validate_prop_name(rules_name)
|
||||
end
|
||||
|
||||
if !rules[:raise_on_nil_write].nil? && rules[:raise_on_nil_write] != true
|
||||
raise ArgumentError.new("The value of `raise_on_nil_write` if specified must be `true` (given: #{rules[:raise_on_nil_write]}).")
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def get_id(instance)
|
||||
prop = prop_by_serialized_forms['_id']
|
||||
if prop
|
||||
get(instance, prop)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
EMPTY_EXTRA_PROPS = {}.freeze
|
||||
private_constant :EMPTY_EXTRA_PROPS
|
||||
|
||||
def extra_props(instance)
|
||||
if instance.instance_variable_defined?(:@_extra_props)
|
||||
instance.instance_variable_get(:@_extra_props) || EMPTY_EXTRA_PROPS
|
||||
else
|
||||
EMPTY_EXTRA_PROPS
|
||||
end
|
||||
end
|
||||
|
||||
# overrides T::Props::PrettyPrintable
|
||||
private def inspect_instance_components(instance, multiline:, indent:)
|
||||
if (extra_props = extra_props(instance)) && !extra_props.empty?
|
||||
pretty_kvs = extra_props.map {|k, v| [k.to_sym, v.inspect]}
|
||||
extra = join_props_with_pretty_values(pretty_kvs, multiline: false)
|
||||
super + ["@_extra_props=<#{extra}>"]
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##############################################
|
||||
|
||||
# NB: This must stay in the same file where T::Props::Serializable is defined due to
|
||||
# T::Props::Decorator#apply_plugin; see https://git.corp.stripe.com/stripe-internal/pay-server/blob/fc7f15593b49875f2d0499ffecfd19798bac05b3/chalk/odm/lib/chalk-odm/document_decorator.rb#L716-L717
|
||||
module T::Props::Serializable::ClassMethods
|
||||
def prop_by_serialized_forms
|
||||
@prop_by_serialized_forms ||= {}
|
||||
end
|
||||
|
||||
# Allocate a new instance and call {#deserialize} to load a new
|
||||
# object from a hash.
|
||||
# @return [Serializable]
|
||||
def from_hash(hash, strict=false)
|
||||
self.decorator.from_hash(hash, strict)
|
||||
end
|
||||
|
||||
# Equivalent to {.from_hash} with `strict` set to true.
|
||||
# @return [Serializable]
|
||||
def from_hash!(hash)
|
||||
self.decorator.from_hash(hash, true)
|
||||
end
|
||||
end
|
@ -0,0 +1,111 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Props::TypeValidation
|
||||
include T::Props::Plugin
|
||||
|
||||
BANNED_TYPES = [Object, BasicObject, Kernel].freeze
|
||||
|
||||
class UnderspecifiedType < ArgumentError; end
|
||||
|
||||
module DecoratorMethods
|
||||
extend T::Sig
|
||||
|
||||
sig {params(key: Symbol).returns(T::Boolean).checked(:never)}
|
||||
def valid_rule_key?(key)
|
||||
super || key == :DEPRECATED_underspecified_type
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
name: T.any(Symbol, String),
|
||||
_cls: Module,
|
||||
rules: T::Hash[Symbol, T.untyped],
|
||||
type: T.any(T::Types::Base, Module)
|
||||
)
|
||||
.void
|
||||
end
|
||||
def prop_validate_definition!(name, _cls, rules, type)
|
||||
super
|
||||
|
||||
if !rules[:DEPRECATED_underspecified_type]
|
||||
validate_type(type, field_name: name)
|
||||
elsif rules[:DEPRECATED_underspecified_type] && find_invalid_subtype(type).nil?
|
||||
raise ArgumentError.new("DEPRECATED_underspecified_type set unnecessarily for #{@class.name}.#{name} - #{type} is a valid type")
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
type: T::Types::Base,
|
||||
field_name: T.any(Symbol, String),
|
||||
)
|
||||
.void
|
||||
end
|
||||
private def validate_type(type, field_name:)
|
||||
if (invalid_subtype = find_invalid_subtype(type))
|
||||
raise UnderspecifiedType.new(type_error_message(invalid_subtype, field_name, type))
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an invalid type, if any, found in the given top-level type.
|
||||
# This might be the type itself, if it is e.g. "Object", or might be
|
||||
# a subtype like the type of the values of a typed hash.
|
||||
#
|
||||
# If the type is fully valid, returns nil.
|
||||
#
|
||||
# checked(:never) - called potentially many times recursively
|
||||
sig {params(type: T::Types::Base).returns(T.nilable(T::Types::Base)).checked(:never)}
|
||||
private def find_invalid_subtype(type)
|
||||
case type
|
||||
when T::Types::TypedEnumerable
|
||||
find_invalid_subtype(type.type)
|
||||
when T::Types::FixedHash
|
||||
type.types.values.map {|subtype| find_invalid_subtype(subtype)}.compact.first
|
||||
when T::Types::Union, T::Types::FixedArray
|
||||
# `T.any` is valid if all of the members are valid
|
||||
type.types.map {|subtype| find_invalid_subtype(subtype)}.compact.first
|
||||
when T::Types::Intersection
|
||||
# `T.all` is valid if at least one of the members is valid
|
||||
invalid = type.types.map {|subtype| find_invalid_subtype(subtype)}.compact
|
||||
if invalid.length == type.types.length
|
||||
invalid.first
|
||||
else
|
||||
nil
|
||||
end
|
||||
when T::Types::Enum, T::Types::ClassOf
|
||||
nil
|
||||
when T::Private::Types::TypeAlias
|
||||
find_invalid_subtype(type.aliased_type)
|
||||
when T::Types::Simple
|
||||
# TODO Could we manage to define a whitelist, consisting of something
|
||||
# like primitives, subdocs, DataInterfaces, and collections/enums/unions
|
||||
# thereof?
|
||||
if BANNED_TYPES.include?(type.raw_type)
|
||||
type
|
||||
else
|
||||
nil
|
||||
end
|
||||
else
|
||||
type
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
type: T::Types::Base,
|
||||
field_name: T.any(Symbol, String),
|
||||
orig_type: T::Types::Base,
|
||||
)
|
||||
.returns(String)
|
||||
end
|
||||
private def type_error_message(type, field_name, orig_type)
|
||||
msg_prefix = "#{@class.name}.#{field_name}: #{orig_type} is invalid in prop definition"
|
||||
if type == orig_type
|
||||
"#{msg_prefix}. Please choose a more specific type (T.untyped and ~equivalents like Object are banned)."
|
||||
else
|
||||
"#{msg_prefix}. Please choose a subtype more specific than #{type} (T.untyped and ~equivalents like Object are banned)."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
59
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/props/utils.rb
vendored
Normal file
59
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/props/utils.rb
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Props::Utils
|
||||
# Deep copy an object. The object must consist of Ruby primitive
|
||||
# types and Hashes and Arrays.
|
||||
def self.deep_clone_object(what, freeze: false)
|
||||
result = case what
|
||||
when true
|
||||
true
|
||||
when false
|
||||
false
|
||||
when Symbol, NilClass, Numeric
|
||||
what
|
||||
when Array
|
||||
what.map {|v| deep_clone_object(v, freeze: freeze)}
|
||||
when Hash
|
||||
h = what.class.new
|
||||
what.each do |k, v|
|
||||
k.freeze if freeze
|
||||
h[k] = deep_clone_object(v, freeze: freeze)
|
||||
end
|
||||
h
|
||||
when Regexp
|
||||
what.dup
|
||||
when T::Enum
|
||||
what
|
||||
else
|
||||
what.clone
|
||||
end
|
||||
freeze ? result.freeze : result
|
||||
end
|
||||
|
||||
# The prop_rules indicate whether we should check for reading a nil value for the prop/field.
|
||||
# This is mostly for the compatibility check that we allow existing documents carry some nil prop/field.
|
||||
def self.need_nil_read_check?(prop_rules)
|
||||
# . :on_load allows nil read, but we need to check for the read for future writes
|
||||
prop_rules[:optional] == :on_load || prop_rules[:raise_on_nil_write]
|
||||
end
|
||||
|
||||
# The prop_rules indicate whether we should check for writing a nil value for the prop/field.
|
||||
def self.need_nil_write_check?(prop_rules)
|
||||
need_nil_read_check?(prop_rules) || T::Props::Utils.required_prop?(prop_rules)
|
||||
end
|
||||
|
||||
def self.required_prop?(prop_rules)
|
||||
# Clients should never reference :_tnilable as the implementation can change.
|
||||
!prop_rules[:_tnilable]
|
||||
end
|
||||
|
||||
def self.optional_prop?(prop_rules)
|
||||
# Clients should never reference :_tnilable as the implementation can change.
|
||||
!!prop_rules[:_tnilable]
|
||||
end
|
||||
|
||||
def self.merge_serialized_optional_rule(prop_rules)
|
||||
{'_tnilable' => true}.merge(prop_rules.merge('_tnilable' => true))
|
||||
end
|
||||
end
|
@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: false
|
||||
|
||||
module T::Props::WeakConstructor
|
||||
include T::Props::Optional
|
||||
extend T::Sig
|
||||
|
||||
# checked(:never) - O(runtime object construction)
|
||||
sig {params(hash: T::Hash[Symbol, T.untyped]).void.checked(:never)}
|
||||
def initialize(hash={})
|
||||
decorator = self.class.decorator
|
||||
|
||||
hash_keys_matching_props = decorator.construct_props_with_defaults(self, hash) +
|
||||
decorator.construct_props_without_defaults(self, hash)
|
||||
|
||||
if hash_keys_matching_props < hash.size
|
||||
raise ArgumentError.new("#{self.class}: Unrecognized properties: #{(hash.keys - decorator.props.keys).join(', ')}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module T::Props::WeakConstructor::DecoratorMethods
|
||||
extend T::Sig
|
||||
|
||||
# Set values for all props that have no defaults. Ignore any not present.
|
||||
#
|
||||
# @return [Integer] A count of props that we successfully initialized (which
|
||||
# we'll use to check for any unrecognized input.)
|
||||
#
|
||||
# checked(:never) - O(runtime object construction)
|
||||
sig {params(instance: T::Props::WeakConstructor, hash: T::Hash[Symbol, T.untyped]).returns(Integer).checked(:never)}
|
||||
def construct_props_without_defaults(instance, hash)
|
||||
# Use `each_pair` rather than `count` because, as of Ruby 2.6, the latter delegates to Enumerator
|
||||
# and therefore allocates for each entry.
|
||||
result = 0
|
||||
props_without_defaults&.each_pair do |p, setter_proc|
|
||||
if hash.key?(p)
|
||||
instance.instance_exec(hash[p], &setter_proc)
|
||||
result += 1
|
||||
end
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
# Set values for all props that have defaults. Use the default if and only if
|
||||
# the prop key isn't in the input.
|
||||
#
|
||||
# @return [Integer] A count of props that we successfully initialized (which
|
||||
# we'll use to check for any unrecognized input.)
|
||||
#
|
||||
# checked(:never) - O(runtime object construction)
|
||||
sig {params(instance: T::Props::WeakConstructor, hash: T::Hash[Symbol, T.untyped]).returns(Integer).checked(:never)}
|
||||
def construct_props_with_defaults(instance, hash)
|
||||
# Use `each_pair` rather than `count` because, as of Ruby 2.6, the latter delegates to Enumerator
|
||||
# and therefore allocates for each entry.
|
||||
result = 0
|
||||
props_with_defaults&.each_pair do |p, default_struct|
|
||||
if hash.key?(p)
|
||||
instance.instance_exec(hash[p], &default_struct.setter_proc)
|
||||
result += 1
|
||||
else
|
||||
default_struct.set_default(instance)
|
||||
end
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
30
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/sig.rb
vendored
Normal file
30
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/sig.rb
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
# Used as a mixin to any class so that you can call `sig`.
|
||||
# Docs at https://sorbet.org/docs/sigs
|
||||
module T::Sig
|
||||
module WithoutRuntime
|
||||
# At runtime, does nothing, but statically it is treated exactly the same
|
||||
# as T::Sig#sig. Only use it in cases where you can't use T::Sig#sig.
|
||||
def self.sig(arg0=nil, &blk); end
|
||||
|
||||
original_verbose = $VERBOSE
|
||||
$VERBOSE = false
|
||||
|
||||
# At runtime, does nothing, but statically it is treated exactly the same
|
||||
# as T::Sig#sig. Only use it in cases where you can't use T::Sig#sig.
|
||||
T::Sig::WithoutRuntime.sig {params(arg0: T.nilable(Symbol), blk: T.proc.bind(T::Private::Methods::DeclBuilder).void).void}
|
||||
def self.sig(arg0=nil, &blk); end # rubocop:disable Lint/DuplicateMethods
|
||||
|
||||
$VERBOSE = original_verbose
|
||||
end
|
||||
|
||||
# Declares a method with type signatures and/or
|
||||
# abstract/override/... helpers. See the documentation URL on
|
||||
# {T::Helpers}
|
||||
T::Sig::WithoutRuntime.sig {params(arg0: T.nilable(Symbol), blk: T.proc.bind(T::Private::Methods::DeclBuilder).void).void}
|
||||
def sig(arg0=nil, &blk)
|
||||
T::Private::Methods.declare_sig(self, Kernel.caller_locations(1, 1)&.first, arg0, &blk)
|
||||
end
|
||||
end
|
51
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/struct.rb
vendored
Normal file
51
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/struct.rb
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
class T::InexactStruct
|
||||
include T::Props
|
||||
include T::Props::Serializable
|
||||
include T::Props::Constructor
|
||||
end
|
||||
|
||||
class T::Struct < T::InexactStruct
|
||||
def self.inherited(subclass)
|
||||
super(subclass)
|
||||
T::Private::ClassUtils.replace_method(subclass.singleton_class, :inherited) do |s|
|
||||
super(s)
|
||||
raise "#{self.name} is a subclass of T::Struct and cannot be subclassed"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class T::ImmutableStruct < T::InexactStruct
|
||||
extend T::Sig
|
||||
|
||||
def self.inherited(subclass)
|
||||
super(subclass)
|
||||
|
||||
T::Private::ClassUtils.replace_method(subclass.singleton_class, :inherited) do |s|
|
||||
super(s)
|
||||
raise "#{self.name} is a subclass of T::ImmutableStruct and cannot be subclassed"
|
||||
end
|
||||
end
|
||||
|
||||
# Matches the one in WeakConstructor, but freezes the object
|
||||
sig {params(hash: T::Hash[Symbol, T.untyped]).void.checked(:never)}
|
||||
def initialize(hash={})
|
||||
super
|
||||
|
||||
freeze
|
||||
end
|
||||
|
||||
# Matches the signature in Props, but raises since this is an immutable struct and only const is allowed
|
||||
sig {params(name: Symbol, cls: T.untyped, rules: T.untyped).void}
|
||||
def self.prop(name, cls, rules={})
|
||||
return super if (cls.is_a?(Hash) && cls[:immutable]) || rules[:immutable]
|
||||
|
||||
raise "Cannot use `prop` in #{self.name} because it is an immutable struct. Use `const` instead"
|
||||
end
|
||||
|
||||
def with(changed_props)
|
||||
raise "Cannot use `with` in #{self.class.name} because it is an immutable struct"
|
||||
end
|
||||
end
|
@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# Modeling AttachedClass properly at runtime would require additional
|
||||
# tracking, so at runtime we permit all values and rely on the static checker.
|
||||
# As AttachedClass is modeled statically as a type member on every singleton
|
||||
# class, this is consistent with the runtime behavior for all type members.
|
||||
class AttachedClassType < Base
|
||||
|
||||
def initialize(); end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T.attached_class"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
true
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
case other
|
||||
when AttachedClassType
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
module Private
|
||||
INSTANCE = AttachedClassType.new.freeze
|
||||
end
|
||||
end
|
||||
end
|
172
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/types/base.rb
vendored
Normal file
172
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/types/base.rb
vendored
Normal file
@ -0,0 +1,172 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
class Base
|
||||
def self.method_added(method_name)
|
||||
super(method_name)
|
||||
# What is now `subtype_of_single?` used to be named `subtype_of?`. Make sure people don't
|
||||
# override the wrong thing.
|
||||
#
|
||||
# NB: Outside of T::Types, we would enforce this by using `sig` and not declaring the method
|
||||
# as overridable, but doing so here would result in a dependency cycle.
|
||||
if method_name == :subtype_of? && self != T::Types::Base
|
||||
raise "`subtype_of?` should not be overridden. You probably want to override " \
|
||||
"`subtype_of_single?` instead."
|
||||
end
|
||||
end
|
||||
|
||||
# this will be redefined in certain subclasses
|
||||
def recursively_valid?(obj)
|
||||
valid?(obj)
|
||||
end
|
||||
|
||||
def valid?(obj)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# @return [T::Boolean] This method must be implemented to return whether the subclass is a subtype
|
||||
# of `type`. This should only be called by `subtype_of?`, which guarantees that `type` will be
|
||||
# a "single" type, by which we mean it won't be a Union or an Intersection (c.f.
|
||||
# `isSubTypeSingle` in sorbet).
|
||||
private def subtype_of_single?(type)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Equality is based on name, so be sure the name reflects all relevant state when implementing.
|
||||
def name
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Mirrors ruby_typer::core::Types::isSubType
|
||||
# See https://git.corp.stripe.com/stripe-internal/ruby-typer/blob/9fc8ed998c04ac0b96592ae6bb3493b8a925c5c1/core/types/subtyping.cc#L912-L950
|
||||
#
|
||||
# This method cannot be overridden (see `method_added` above).
|
||||
# Subclasses only need to implement `subtype_of_single?`).
|
||||
def subtype_of?(t2)
|
||||
t1 = self
|
||||
|
||||
if t2.is_a?(T::Private::Types::TypeAlias)
|
||||
t2 = t2.aliased_type
|
||||
end
|
||||
|
||||
if t1.is_a?(T::Private::Types::TypeAlias)
|
||||
return t1.aliased_type.subtype_of?(t2)
|
||||
end
|
||||
|
||||
# pairs to cover: 1 (_, _)
|
||||
# 2 (_, And)
|
||||
# 3 (_, Or)
|
||||
# 4 (And, _)
|
||||
# 5 (And, And)
|
||||
# 6 (And, Or)
|
||||
# 7 (Or, _)
|
||||
# 8 (Or, And)
|
||||
# 9 (Or, Or)
|
||||
|
||||
# Note: order of cases here matters!
|
||||
if t1.is_a?(T::Types::Union) # 7, 8, 9
|
||||
# this will be incorrect if/when we have Type members
|
||||
return t1.types.all? {|t1_member| t1_member.subtype_of?(t2)}
|
||||
end
|
||||
|
||||
if t2.is_a?(T::Types::Intersection) # 2, 5
|
||||
# this will be incorrect if/when we have Type members
|
||||
return t2.types.all? {|t2_member| t1.subtype_of?(t2_member)}
|
||||
end
|
||||
|
||||
if t2.is_a?(T::Types::Union)
|
||||
if t1.is_a?(T::Types::Intersection) # 6
|
||||
# dropping either of parts eagerly make subtype test be too strict.
|
||||
# we have to try both cases, when we normally try only one
|
||||
return t2.types.any? {|t2_member| t1.subtype_of?(t2_member)} ||
|
||||
t1.types.any? {|t1_member| t1_member.subtype_of?(t2)}
|
||||
end
|
||||
return t2.types.any? {|t2_member| t1.subtype_of?(t2_member)} # 3
|
||||
end
|
||||
|
||||
if t1.is_a?(T::Types::Intersection) # 4
|
||||
# this will be incorrect if/when we have Type members
|
||||
return t1.types.any? {|t1_member| t1_member.subtype_of?(t2)}
|
||||
end
|
||||
|
||||
# 1; Start with some special cases
|
||||
if t1.is_a?(T::Private::Types::Void)
|
||||
return t2.is_a?(T::Private::Types::Void)
|
||||
end
|
||||
|
||||
if t1.is_a?(T::Types::Untyped) || t2.is_a?(T::Types::Untyped)
|
||||
return true
|
||||
end
|
||||
|
||||
# Rest of (1)
|
||||
subtype_of_single?(t2)
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def describe_obj(obj)
|
||||
# Would be redundant to print class and value in these common cases.
|
||||
case obj
|
||||
when nil, true, false
|
||||
return "type #{obj.class}"
|
||||
end
|
||||
|
||||
# In rare cases, obj.inspect may fail, or be undefined, so rescue.
|
||||
begin
|
||||
# Default inspect behavior of, eg; `#<Object:0x0...>` is ugly; just print the hash instead, which is more concise/readable.
|
||||
if obj.method(:inspect).owner == Kernel
|
||||
"type #{obj.class} with hash #{obj.hash}"
|
||||
elsif T::Configuration.include_value_in_type_errors?
|
||||
"type #{obj.class} with value #{T::Utils.string_truncate_middle(obj.inspect, 30, 30)}"
|
||||
else
|
||||
"type #{obj.class}"
|
||||
end
|
||||
rescue StandardError, SystemStackError
|
||||
"type #{obj.class} with unprintable value"
|
||||
end
|
||||
end
|
||||
|
||||
def error_message_for_obj(obj)
|
||||
if valid?(obj)
|
||||
nil
|
||||
else
|
||||
error_message(obj)
|
||||
end
|
||||
end
|
||||
|
||||
def error_message_for_obj_recursive(obj)
|
||||
if recursively_valid?(obj)
|
||||
nil
|
||||
else
|
||||
error_message(obj)
|
||||
end
|
||||
end
|
||||
|
||||
private def error_message(obj)
|
||||
"Expected type #{self.name}, got #{describe_obj(obj)}"
|
||||
end
|
||||
|
||||
def validate!(obj)
|
||||
err = error_message_for_obj(obj)
|
||||
raise TypeError.new(err) if err
|
||||
end
|
||||
|
||||
### Equality methods (necessary for deduping types with `uniq`)
|
||||
|
||||
def hash
|
||||
name.hash
|
||||
end
|
||||
|
||||
# Type equivalence, defined by serializing the type to a string (with
|
||||
# `#name`) and comparing the resulting strings for equality.
|
||||
def ==(other)
|
||||
(T::Utils.resolve_alias(other).class == T::Utils.resolve_alias(self).class) &&
|
||||
other.name == self.name
|
||||
end
|
||||
|
||||
alias_method :eql?, :==
|
||||
end
|
||||
end
|
@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# Validates that an object belongs to the specified class.
|
||||
class ClassOf < Base
|
||||
attr_reader :type
|
||||
|
||||
def initialize(type)
|
||||
@type = type
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T.class_of(#{@type})"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
obj.is_a?(Module) && obj <= @type
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def subtype_of_single?(other)
|
||||
case other
|
||||
when ClassOf
|
||||
@type <= other.type
|
||||
when Simple
|
||||
@type.is_a?(other.raw_type)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def describe_obj(obj)
|
||||
obj.inspect
|
||||
end
|
||||
end
|
||||
end
|
40
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/types/enum.rb
vendored
Normal file
40
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/types/enum.rb
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# validates that the provided value is within a given set/enum
|
||||
class Enum < Base
|
||||
extend T::Sig
|
||||
|
||||
attr_reader :values
|
||||
|
||||
def initialize(values)
|
||||
@values = values
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
@values.member?(obj)
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
case other
|
||||
when Enum
|
||||
(other.values - @values).empty?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T.deprecated_enum([#{@values.map(&:inspect).join(', ')}])"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def describe_obj(obj)
|
||||
obj.inspect
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,86 @@
|
||||
# frozen_string_literal: true
|
||||
# https://jira.corp.stripe.com/browse/RUBYPLAT-1107
|
||||
# typed: false
|
||||
|
||||
module T::Types
|
||||
# Takes a list of types. Validates each item in an array using the type in the same position
|
||||
# in the list.
|
||||
class FixedArray < Base
|
||||
attr_reader :types
|
||||
|
||||
def initialize(types)
|
||||
@types = types.map {|type| T::Utils.coerce(type)}
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"[#{@types.join(', ')}]"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def recursively_valid?(obj)
|
||||
if obj.is_a?(Array) && obj.length == @types.length
|
||||
i = 0
|
||||
while i < @types.length
|
||||
if !@types[i].recursively_valid?(obj[i])
|
||||
return false
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
if obj.is_a?(Array) && obj.length == @types.length
|
||||
i = 0
|
||||
while i < @types.length
|
||||
if !@types[i].valid?(obj[i])
|
||||
return false
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
case other
|
||||
when FixedArray
|
||||
# Properly speaking, covariance here is unsound since arrays
|
||||
# can be mutated, but sorbet implements covariant tuples for
|
||||
# ease of adoption.
|
||||
@types.size == other.types.size && @types.zip(other.types).all? do |t1, t2|
|
||||
t1.subtype_of?(t2)
|
||||
end
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# This gives us better errors, e.g.:
|
||||
# "Expected [String, Symbol], got [String, String]"
|
||||
# instead of
|
||||
# "Expected [String, Symbol], got Array".
|
||||
#
|
||||
# overrides Base
|
||||
def describe_obj(obj)
|
||||
if obj.is_a?(Array)
|
||||
if obj.length == @types.length
|
||||
item_classes = obj.map(&:class).join(', ')
|
||||
"type [#{item_classes}]"
|
||||
else
|
||||
"array of size #{obj.length}"
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# Takes a hash of types. Validates each item in a hash using the type in the same position
|
||||
# in the list.
|
||||
class FixedHash < Base
|
||||
attr_reader :types
|
||||
|
||||
def initialize(types)
|
||||
@types = types.transform_values {|v| T::Utils.coerce(v)}
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
serialize_hash(@types)
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def recursively_valid?(obj)
|
||||
return false unless obj.is_a?(Hash)
|
||||
return false if @types.any? {|key, type| !type.recursively_valid?(obj[key])}
|
||||
return false if obj.any? {|key, _| !@types[key]}
|
||||
true
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
return false unless obj.is_a?(Hash)
|
||||
return false if @types.any? {|key, type| !type.valid?(obj[key])}
|
||||
return false if obj.any? {|key, _| !@types[key]}
|
||||
true
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
case other
|
||||
when FixedHash
|
||||
# Using `subtype_of?` here instead of == would be unsound
|
||||
@types == other.types
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# This gives us better errors, e.g.:
|
||||
# `Expected {a: String}, got {a: TrueClass}`
|
||||
# instead of
|
||||
# `Expected {a: String}, got Hash`.
|
||||
#
|
||||
# overrides Base
|
||||
def describe_obj(obj)
|
||||
if obj.is_a?(Hash)
|
||||
"type #{serialize_hash(obj.transform_values(&:class))}"
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialize_hash(hash)
|
||||
entries = hash.map do |(k, v)|
|
||||
if Symbol === k && ":#{k}" == k.inspect
|
||||
"#{k}: #{v}"
|
||||
else
|
||||
"#{k.inspect} => #{v}"
|
||||
end
|
||||
end
|
||||
|
||||
"{#{entries.join(', ')}}"
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# Takes a list of types. Validates that an object matches all of the types.
|
||||
class Intersection < Base
|
||||
attr_reader :types
|
||||
|
||||
def initialize(types)
|
||||
@types = types.flat_map do |type|
|
||||
type = T::Utils.resolve_alias(type)
|
||||
if type.is_a?(Intersection)
|
||||
# Simplify nested intersections (mostly so `name` returns a nicer value)
|
||||
type.types
|
||||
else
|
||||
T::Utils.coerce(type)
|
||||
end
|
||||
end.uniq
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T.all(#{@types.map(&:name).sort.join(', ')})"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def recursively_valid?(obj)
|
||||
@types.all? {|type| type.recursively_valid?(obj)}
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
@types.all? {|type| type.valid?(obj)}
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
raise "This should never be reached if you're going through `subtype_of?` (and you should be)"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# The bottom type
|
||||
class NoReturn < Base
|
||||
|
||||
def initialize; end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T.noreturn"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
false
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
true
|
||||
end
|
||||
|
||||
module Private
|
||||
INSTANCE = NoReturn.new.freeze
|
||||
end
|
||||
end
|
||||
end
|
51
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/types/proc.rb
vendored
Normal file
51
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/types/proc.rb
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# Defines the type of a proc (a ruby callable). At runtime, only
|
||||
# validates that the value is a `::Proc`.
|
||||
#
|
||||
# At present, we only support fixed-arity procs with no optional or
|
||||
# keyword arguments.
|
||||
class Proc < Base
|
||||
attr_reader :arg_types
|
||||
attr_reader :returns
|
||||
|
||||
def initialize(arg_types, returns)
|
||||
@arg_types = {}
|
||||
arg_types.each do |key, raw_type|
|
||||
@arg_types[key] = T::Utils.coerce(raw_type)
|
||||
end
|
||||
@returns = T::Utils.coerce(returns)
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
args = []
|
||||
@arg_types.each do |k, v|
|
||||
args << "#{k}: #{v.name}"
|
||||
end
|
||||
"T.proc.params(#{args.join(', ')}).returns(#{returns})"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
obj.is_a?(::Proc)
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
case other
|
||||
when self.class
|
||||
if arg_types.size != other.arg_types.size
|
||||
return false
|
||||
end
|
||||
arg_types.values.zip(other.arg_types.values).all? do |a, b|
|
||||
b.subtype_of?(a)
|
||||
end && returns.subtype_of?(other.returns)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# Modeling self-types properly at runtime would require additional tracking,
|
||||
# so at runtime we permit all values and rely on the static checker.
|
||||
class SelfType < Base
|
||||
|
||||
def initialize(); end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T.self_type"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
true
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
case other
|
||||
when SelfType
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
module Private
|
||||
INSTANCE = SelfType.new.freeze
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,94 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# Validates that an object belongs to the specified class.
|
||||
class Simple < Base
|
||||
attr_reader :raw_type
|
||||
|
||||
def initialize(raw_type)
|
||||
@raw_type = raw_type
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
# Memoize to mitigate pathological performance with anonymous modules (https://bugs.ruby-lang.org/issues/11119)
|
||||
#
|
||||
# `name` isn't normally a hot path for types, but it is used in initializing a T::Types::Union,
|
||||
# and so in `T.nilable`, and so in runtime constructions like `x = T.let(nil, T.nilable(Integer))`.
|
||||
@name ||= @raw_type.name.freeze
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
obj.is_a?(@raw_type)
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
case other
|
||||
when Simple
|
||||
@raw_type <= other.raw_type
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def error_message(obj)
|
||||
error_message = super(obj)
|
||||
actual_name = obj.class.name
|
||||
|
||||
return error_message unless name == actual_name
|
||||
|
||||
<<~MSG.strip
|
||||
#{error_message}
|
||||
|
||||
The expected type and received object type have the same name but refer to different constants.
|
||||
Expected type is #{name} with object id #{@raw_type.__id__}, but received type is #{actual_name} with object id #{obj.class.__id__}.
|
||||
|
||||
There might be a constant reloading problem in your application.
|
||||
MSG
|
||||
end
|
||||
|
||||
def to_nilable
|
||||
@nilable ||= T::Types::Union.new([self, T::Utils::Nilable::NIL_TYPE])
|
||||
end
|
||||
|
||||
module Private
|
||||
module Pool
|
||||
@cache = ObjectSpace::WeakMap.new
|
||||
|
||||
def self.type_for_module(mod)
|
||||
cached = @cache[mod]
|
||||
return cached if cached
|
||||
|
||||
type = if mod == ::Array
|
||||
T::Array[T.untyped]
|
||||
elsif mod == ::Hash
|
||||
T::Hash[T.untyped, T.untyped]
|
||||
elsif mod == ::Enumerable
|
||||
T::Enumerable[T.untyped]
|
||||
elsif mod == ::Enumerator
|
||||
T::Enumerator[T.untyped]
|
||||
elsif mod == ::Range
|
||||
T::Range[T.untyped]
|
||||
elsif !Object.autoload?(:Set) && Object.const_defined?(:Set) && mod == ::Set
|
||||
T::Set[T.untyped]
|
||||
else
|
||||
Simple.new(mod)
|
||||
end
|
||||
|
||||
# Unfortunately, we still need to check if the module is frozen,
|
||||
# since WeakMap adds a finalizer to the key that is added
|
||||
# to the map, so that it can clear the map entry when the key is
|
||||
# garbage collected.
|
||||
# For a frozen object, though, adding a finalizer is not a valid
|
||||
# operation, so this still raises if `mod` is frozen.
|
||||
@cache[mod] = type unless mod.frozen?
|
||||
type
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# Validates that an object is equal to another T::Enum singleton value.
|
||||
class TEnum < Base
|
||||
attr_reader :val
|
||||
|
||||
def initialize(val)
|
||||
@val = val
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
# Strips the #<...> off, just leaving the ...
|
||||
# Reasoning: the user will have written something like
|
||||
# T.any(MyEnum::A, MyEnum::B)
|
||||
# in the type, so we should print what they wrote in errors, not:
|
||||
# T.any(#<MyEnum::A>, #<MyEnum::B>)
|
||||
@val.inspect[2..-2]
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
@val == obj
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
case other
|
||||
when TEnum
|
||||
@val == other.val
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
module T::Types
|
||||
class TypeMember < TypeVariable
|
||||
end
|
||||
end
|
@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
class TypeParameter < Base
|
||||
def initialize(name)
|
||||
raise ArgumentError.new("not a symbol: #{name}") unless name.is_a?(Symbol)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def valid?(obj)
|
||||
true
|
||||
end
|
||||
|
||||
def subtype_of_single?(type)
|
||||
true
|
||||
end
|
||||
|
||||
def name
|
||||
"T.type_parameter(:#{@name})"
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: strict
|
||||
|
||||
module T::Types
|
||||
class TypeTemplate < TypeVariable
|
||||
end
|
||||
end
|
@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# Since we do type erasure at runtime, this just validates the variance and
|
||||
# provides some syntax for the static type checker
|
||||
class TypeVariable < Base
|
||||
attr_reader :variance
|
||||
|
||||
VALID_VARIANCES = %i[in out invariant].freeze
|
||||
|
||||
def initialize(variance)
|
||||
case variance
|
||||
when Hash then raise ArgumentError.new("Pass bounds using a block. Got: #{variance}")
|
||||
when *VALID_VARIANCES then nil
|
||||
else
|
||||
raise TypeError.new("invalid variance #{variance}")
|
||||
end
|
||||
@variance = variance
|
||||
end
|
||||
|
||||
def valid?(obj)
|
||||
true
|
||||
end
|
||||
|
||||
def subtype_of_single?(type)
|
||||
true
|
||||
end
|
||||
|
||||
def name
|
||||
Untyped.new.name
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
class TypedArray < TypedEnumerable
|
||||
# overrides Base
|
||||
def name
|
||||
"T::Array[#{@type.name}]"
|
||||
end
|
||||
|
||||
def underlying_class
|
||||
Array
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def recursively_valid?(obj)
|
||||
obj.is_a?(Array) && super
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
obj.is_a?(Array)
|
||||
end
|
||||
|
||||
def new(*args)
|
||||
Array.new(*T.unsafe(args))
|
||||
end
|
||||
|
||||
class Untyped < TypedArray
|
||||
def initialize
|
||||
super(T.untyped)
|
||||
end
|
||||
|
||||
def valid?(obj)
|
||||
obj.is_a?(Array)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,176 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# Note: All subclasses of Enumerable should add themselves to the
|
||||
# `case` statement below in `describe_obj` in order to get better
|
||||
# error messages.
|
||||
class TypedEnumerable < Base
|
||||
attr_reader :type
|
||||
|
||||
def initialize(type)
|
||||
@type = T::Utils.coerce(type)
|
||||
end
|
||||
|
||||
def underlying_class
|
||||
Enumerable
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T::Enumerable[#{@type.name}]"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
obj.is_a?(Enumerable)
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def recursively_valid?(obj)
|
||||
return false unless obj.is_a?(Enumerable)
|
||||
case obj
|
||||
when Array
|
||||
begin
|
||||
it = 0
|
||||
while it < obj.count
|
||||
return false unless @type.recursively_valid?(obj[it])
|
||||
it += 1
|
||||
end
|
||||
true
|
||||
end
|
||||
when Hash
|
||||
return false unless @type.is_a?(FixedArray)
|
||||
types = @type.types
|
||||
return false if types.count != 2
|
||||
key_type = types[0]
|
||||
value_type = types[1]
|
||||
obj.each_pair do |key, val|
|
||||
# Some objects (I'm looking at you Rack::Utils::HeaderHash) don't
|
||||
# iterate over a [key, value] array, so we can't juse use the @type.recursively_valid?(v)
|
||||
return false if !key_type.recursively_valid?(key) || !value_type.recursively_valid?(val)
|
||||
end
|
||||
true
|
||||
when Enumerator::Lazy
|
||||
# Enumerators can be unbounded: see `[:foo, :bar].cycle`
|
||||
true
|
||||
when Enumerator
|
||||
# Enumerators can be unbounded: see `[:foo, :bar].cycle`
|
||||
true
|
||||
when Range
|
||||
# A nil beginning or a nil end does not provide any type information. That is, nil in a range represents
|
||||
# boundlessness, it does not express a type. For example `(nil...nil)` is not a T::Range[NilClass], its a range
|
||||
# of unknown types (T::Range[T.untyped]).
|
||||
# Similarly, `(nil...1)` is not a `T::Range[T.nilable(Integer)]`, it's a boundless range of Integer.
|
||||
(obj.begin.nil? || @type.recursively_valid?(obj.begin)) && (obj.end.nil? || @type.recursively_valid?(obj.end))
|
||||
when Set
|
||||
obj.each do |item|
|
||||
return false unless @type.recursively_valid?(item)
|
||||
end
|
||||
|
||||
true
|
||||
else
|
||||
# We don't check the enumerable since it isn't guaranteed to be
|
||||
# rewindable (e.g. STDIN) and it may be expensive to enumerate
|
||||
# (e.g. an enumerator that makes an HTTP request)"
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
if other.class <= TypedEnumerable &&
|
||||
underlying_class <= other.underlying_class
|
||||
# Enumerables are covariant because they are read only
|
||||
#
|
||||
# Properly speaking, many Enumerable subtypes (e.g. Set)
|
||||
# should be invariant because they are mutable and support
|
||||
# both reading and writing. However, Sorbet treats *all*
|
||||
# Enumerable subclasses as covariant for ease of adoption.
|
||||
@type.subtype_of?(other.type)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def describe_obj(obj)
|
||||
return super unless obj.is_a?(Enumerable)
|
||||
type_from_instance(obj).name
|
||||
end
|
||||
|
||||
private def type_from_instances(objs)
|
||||
return objs.class unless objs.is_a?(Enumerable)
|
||||
obtained_types = []
|
||||
begin
|
||||
objs.each do |x|
|
||||
obtained_types << type_from_instance(x)
|
||||
end
|
||||
rescue
|
||||
return T.untyped # all we can do is go with the types we have so far
|
||||
end
|
||||
if obtained_types.count > 1
|
||||
# Multiple kinds of bad types showed up, we'll suggest a union
|
||||
# type you might want.
|
||||
Union.new(obtained_types)
|
||||
elsif obtained_types.empty?
|
||||
T.noreturn
|
||||
else
|
||||
# Everything was the same bad type, lets just show that
|
||||
obtained_types.first
|
||||
end
|
||||
end
|
||||
|
||||
private def type_from_instance(obj)
|
||||
if [true, false].include?(obj)
|
||||
return T::Boolean
|
||||
elsif !obj.is_a?(Enumerable)
|
||||
return obj.class
|
||||
end
|
||||
|
||||
case obj
|
||||
when Array
|
||||
T::Array[type_from_instances(obj)]
|
||||
when Hash
|
||||
inferred_key = type_from_instances(obj.keys)
|
||||
inferred_val = type_from_instances(obj.values)
|
||||
T::Hash[inferred_key, inferred_val]
|
||||
when Range
|
||||
# We can't get any information from `NilClass` in ranges (since nil is used to represent boundlessness).
|
||||
typeable_objects = [obj.begin, obj.end].compact
|
||||
if typeable_objects.empty?
|
||||
T::Range[T.untyped]
|
||||
else
|
||||
T::Range[type_from_instances(typeable_objects)]
|
||||
end
|
||||
when Enumerator::Lazy
|
||||
T::Enumerator::Lazy[type_from_instances(obj)]
|
||||
when Enumerator
|
||||
T::Enumerator[type_from_instances(obj)]
|
||||
when Set
|
||||
T::Set[type_from_instances(obj)]
|
||||
when IO
|
||||
# Short circuit for anything IO-like (File, etc.). In these cases,
|
||||
# enumerating the object is a destructive operation and might hang.
|
||||
obj.class
|
||||
else
|
||||
# This is a specialized enumerable type, just return the class.
|
||||
if T::Configuration::AT_LEAST_RUBY_2_7
|
||||
Object.instance_method(:class).bind_call(obj)
|
||||
else
|
||||
Object.instance_method(:class).bind(obj).call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Untyped < TypedEnumerable
|
||||
def initialize
|
||||
super(T.untyped)
|
||||
end
|
||||
|
||||
def valid?(obj)
|
||||
obj.is_a?(Enumerable)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
class TypedEnumerator < TypedEnumerable
|
||||
attr_reader :type
|
||||
|
||||
def underlying_class
|
||||
Enumerator
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T::Enumerator[#{@type.name}]"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def recursively_valid?(obj)
|
||||
obj.is_a?(Enumerator) && super
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
obj.is_a?(Enumerator)
|
||||
end
|
||||
|
||||
def new(*args, &blk)
|
||||
T.unsafe(Enumerator).new(*args, &blk)
|
||||
end
|
||||
|
||||
class Untyped < TypedEnumerator
|
||||
def initialize
|
||||
super(T.untyped)
|
||||
end
|
||||
|
||||
def valid?(obj)
|
||||
obj.is_a?(Enumerator)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
class TypedEnumeratorLazy < TypedEnumerable
|
||||
attr_reader :type
|
||||
|
||||
def underlying_class
|
||||
Enumerator::Lazy
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T::Enumerator::Lazy[#{@type.name}]"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def recursively_valid?(obj)
|
||||
obj.is_a?(Enumerator::Lazy) && super
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
obj.is_a?(Enumerator::Lazy)
|
||||
end
|
||||
|
||||
def new(*args, &blk)
|
||||
T.unsafe(Enumerator::Lazy).new(*args, &blk)
|
||||
end
|
||||
|
||||
class Untyped < TypedEnumeratorLazy
|
||||
def initialize
|
||||
super(T.untyped)
|
||||
end
|
||||
|
||||
def valid?(obj)
|
||||
obj.is_a?(Enumerator::Lazy)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
class TypedHash < TypedEnumerable
|
||||
# Technically we don't need these, but they are a nice api
|
||||
attr_reader :keys, :values
|
||||
|
||||
def underlying_class
|
||||
Hash
|
||||
end
|
||||
|
||||
def initialize(keys:, values:)
|
||||
@keys = T::Utils.coerce(keys)
|
||||
@values = T::Utils.coerce(values)
|
||||
@type = T::Utils.coerce([keys, values])
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T::Hash[#{@keys.name}, #{@values.name}]"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def recursively_valid?(obj)
|
||||
obj.is_a?(Hash) && super
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
obj.is_a?(Hash)
|
||||
end
|
||||
|
||||
def new(*args, &blk)
|
||||
Hash.new(*T.unsafe(args), &blk)
|
||||
end
|
||||
|
||||
class Untyped < TypedHash
|
||||
def initialize
|
||||
super(keys: T.untyped, values: T.untyped)
|
||||
end
|
||||
|
||||
def valid?(obj)
|
||||
obj.is_a?(Hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
class TypedRange < TypedEnumerable
|
||||
attr_reader :type
|
||||
|
||||
def underlying_class
|
||||
Hash
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T::Range[#{@type.name}]"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def recursively_valid?(obj)
|
||||
obj.is_a?(Range) && super
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
obj.is_a?(Range)
|
||||
end
|
||||
|
||||
def new(*args)
|
||||
T.unsafe(Range).new(*args)
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
class TypedSet < TypedEnumerable
|
||||
attr_reader :type
|
||||
|
||||
def underlying_class
|
||||
Hash
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T::Set[#{@type.name}]"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def recursively_valid?(obj)
|
||||
# Re-implements non_forcing_is_a?
|
||||
return false if Object.autoload?(:Set) # Set is meant to be autoloaded but not yet loaded, this value can't be a Set
|
||||
return false unless Object.const_defined?(:Set) # Set is not loaded yet
|
||||
obj.is_a?(Set) && super
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
# Re-implements non_forcing_is_a?
|
||||
return false if Object.autoload?(:Set) # Set is meant to be autoloaded but not yet loaded, this value can't be a Set
|
||||
return false unless Object.const_defined?(:Set) # Set is not loaded yet
|
||||
obj.is_a?(Set)
|
||||
end
|
||||
|
||||
def new(*args)
|
||||
# Fine for this to blow up, because hopefully if they're trying to make a
|
||||
# Set, they don't mind putting (or already have put) a `require 'set'` in
|
||||
# their program directly.
|
||||
Set.new(*T.unsafe(args))
|
||||
end
|
||||
|
||||
class Untyped < TypedSet
|
||||
def initialize
|
||||
super(T.untyped)
|
||||
end
|
||||
|
||||
def valid?(obj)
|
||||
# Re-implements non_forcing_is_a?
|
||||
return false if Object.autoload?(:Set) # Set is meant to be autoloaded but not yet loaded, this value can't be a Set
|
||||
return false unless Object.const_defined?(:Set) # Set is not loaded yet
|
||||
obj.is_a?(Set)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
91
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/types/union.rb
vendored
Normal file
91
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/types/union.rb
vendored
Normal file
@ -0,0 +1,91 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# Takes a list of types. Validates that an object matches at least one of the types.
|
||||
class Union < Base
|
||||
attr_reader :types
|
||||
|
||||
def initialize(types)
|
||||
@types = types.flat_map do |type|
|
||||
type = T::Utils.coerce(type)
|
||||
if type.is_a?(Union)
|
||||
# Simplify nested unions (mostly so `name` returns a nicer value)
|
||||
type.types
|
||||
else
|
||||
type
|
||||
end
|
||||
end.uniq
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
type_shortcuts(@types)
|
||||
end
|
||||
|
||||
private def type_shortcuts(types)
|
||||
if types.size == 1
|
||||
return types[0].name
|
||||
end
|
||||
nilable = T::Utils.coerce(NilClass)
|
||||
trueclass = T::Utils.coerce(TrueClass)
|
||||
falseclass = T::Utils.coerce(FalseClass)
|
||||
if types.any? {|t| t == nilable}
|
||||
remaining_types = types.reject {|t| t == nilable}
|
||||
"T.nilable(#{type_shortcuts(remaining_types)})"
|
||||
elsif types.any? {|t| t == trueclass} && types.any? {|t| t == falseclass}
|
||||
remaining_types = types.reject {|t| t == trueclass || t == falseclass}
|
||||
type_shortcuts([T::Private::Types::StringHolder.new("T::Boolean")] + remaining_types)
|
||||
else
|
||||
names = types.map(&:name).compact.sort
|
||||
"T.any(#{names.join(', ')})"
|
||||
end
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def recursively_valid?(obj)
|
||||
@types.any? {|type| type.recursively_valid?(obj)}
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
@types.any? {|type| type.valid?(obj)}
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
raise "This should never be reached if you're going through `subtype_of?` (and you should be)"
|
||||
end
|
||||
|
||||
module Private
|
||||
module Pool
|
||||
EMPTY_ARRAY = [].freeze
|
||||
private_constant :EMPTY_ARRAY
|
||||
|
||||
# @param type_a [T::Types::Base]
|
||||
# @param type_b [T::Types::Base]
|
||||
# @param types [Array] optional array of additional T::Types::Base instances
|
||||
def self.union_of_types(type_a, type_b, types=EMPTY_ARRAY)
|
||||
if types.empty?
|
||||
# We aren't guaranteed to detect a simple `T.nilable(<Module>)` type here
|
||||
# in cases where there are duplicate types, nested unions, etc.
|
||||
#
|
||||
# That's ok, because this is an optimization which isn't necessary for
|
||||
# correctness.
|
||||
if type_b == T::Utils::Nilable::NIL_TYPE && type_a.is_a?(T::Types::Simple)
|
||||
type_a.to_nilable
|
||||
elsif type_a == T::Utils::Nilable::NIL_TYPE && type_b.is_a?(T::Types::Simple)
|
||||
type_b.to_nilable
|
||||
else
|
||||
Union.new([type_a, type_b])
|
||||
end
|
||||
else
|
||||
# This can't be a `T.nilable(<Module>)` case unless there are duplicates,
|
||||
# which is possible but unexpected.
|
||||
Union.new([type_a, type_b] + types)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Types
|
||||
# A dynamic type, which permits whatever
|
||||
class Untyped < Base
|
||||
|
||||
def initialize; end
|
||||
|
||||
# overrides Base
|
||||
def name
|
||||
"T.untyped"
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
def valid?(obj)
|
||||
true
|
||||
end
|
||||
|
||||
# overrides Base
|
||||
private def subtype_of_single?(other)
|
||||
true
|
||||
end
|
||||
|
||||
module Private
|
||||
INSTANCE = Untyped.new.freeze
|
||||
end
|
||||
end
|
||||
end
|
209
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/utils.rb
vendored
Normal file
209
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/sorbet-runtime-0.5.10160/lib/types/utils.rb
vendored
Normal file
@ -0,0 +1,209 @@
|
||||
# frozen_string_literal: true
|
||||
# typed: true
|
||||
|
||||
module T::Utils
|
||||
# Used to convert from a type specification to a `T::Types::Base`.
|
||||
def self.coerce(val)
|
||||
if val.is_a?(T::Private::Types::TypeAlias)
|
||||
val.aliased_type
|
||||
elsif val.is_a?(T::Types::Base)
|
||||
val
|
||||
elsif val.is_a?(Module)
|
||||
T::Types::Simple::Private::Pool.type_for_module(val)
|
||||
elsif val.is_a?(::Array)
|
||||
T::Types::FixedArray.new(val)
|
||||
elsif val.is_a?(::Hash)
|
||||
T::Types::FixedHash.new(val)
|
||||
elsif val.is_a?(T::Private::Methods::DeclBuilder)
|
||||
T::Private::Methods.finalize_proc(val.decl)
|
||||
elsif val.is_a?(::T::Enum)
|
||||
T::Types::TEnum.new(val)
|
||||
elsif val.is_a?(::String)
|
||||
raise "Invalid String literal for type constraint. Must be an #{T::Types::Base}, a " \
|
||||
"class/module, or an array. Got a String with value `#{val}`."
|
||||
else
|
||||
raise "Invalid value for type constraint. Must be an #{T::Types::Base}, a " \
|
||||
"class/module, or an array. Got a `#{val.class}`."
|
||||
end
|
||||
end
|
||||
|
||||
# Dynamically confirm that `value` is recursively a valid value of
|
||||
# type `type`, including recursively through collections. Note that
|
||||
# in some cases this runtime check can be very expensive, especially
|
||||
# with large collections of objects.
|
||||
def self.check_type_recursive!(value, type)
|
||||
T::Private::Casts.cast_recursive(value, type, cast_method: "T.check_type_recursive!")
|
||||
end
|
||||
|
||||
# Returns the set of all methods (public, protected, private) defined on a module or its
|
||||
# ancestors, excluding Object and its ancestors. Overrides of methods from Object (and its
|
||||
# ancestors) are included.
|
||||
def self.methods_excluding_object(mod)
|
||||
# We can't just do mod.instance_methods - Object.instance_methods, because that would leave out
|
||||
# any methods from Object that are overridden in mod.
|
||||
mod.ancestors.flat_map do |ancestor|
|
||||
# equivalent to checking Object.ancestors.include?(ancestor)
|
||||
next [] if Object <= ancestor
|
||||
ancestor.instance_methods(false) + ancestor.private_instance_methods(false)
|
||||
end.uniq
|
||||
end
|
||||
|
||||
# Returns the signature for the `UnboundMethod`, or nil if it's not sig'd
|
||||
#
|
||||
# @example T::Utils.signature_for_method(x.method(:foo))
|
||||
def self.signature_for_method(method)
|
||||
T::Private::Methods.signature_for_method(method)
|
||||
end
|
||||
|
||||
# Returns the signature for the instance method on the supplied module, or nil if it's not found or not typed.
|
||||
#
|
||||
# @example T::Utils.signature_for_instance_method(MyClass, :my_method)
|
||||
def self.signature_for_instance_method(mod, method_name)
|
||||
T::Private::Methods.signature_for_method(mod.instance_method(method_name))
|
||||
end
|
||||
|
||||
def self.wrap_method_with_call_validation_if_needed(mod, method_sig, original_method)
|
||||
T::Private::Methods::CallValidation.wrap_method_if_needed(mod, method_sig, original_method)
|
||||
end
|
||||
|
||||
# Unwraps all the sigs.
|
||||
def self.run_all_sig_blocks
|
||||
T::Private::Methods.run_all_sig_blocks
|
||||
end
|
||||
|
||||
# Return the underlying type for a type alias. Otherwise returns type.
|
||||
def self.resolve_alias(type)
|
||||
case type
|
||||
when T::Private::Types::TypeAlias
|
||||
type.aliased_type
|
||||
else
|
||||
type
|
||||
end
|
||||
end
|
||||
|
||||
# Give a type which is a subclass of T::Types::Base, determines if the type is a simple nilable type (union of NilClass and something else).
|
||||
# If so, returns the T::Types::Base of the something else. Otherwise, returns nil.
|
||||
def self.unwrap_nilable(type)
|
||||
case type
|
||||
when T::Types::Union
|
||||
non_nil_types = type.types.reject {|t| t == Nilable::NIL_TYPE}
|
||||
return nil if type.types.length == non_nil_types.length
|
||||
case non_nil_types.length
|
||||
when 0 then nil
|
||||
when 1 then non_nil_types.first
|
||||
else
|
||||
T::Types::Union::Private::Pool.union_of_types(non_nil_types[0], non_nil_types[1], non_nil_types[2..-1])
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the arity of a method, unwrapping the sig if needed
|
||||
def self.arity(method)
|
||||
arity = method.arity
|
||||
return arity if arity != -1 || method.is_a?(Proc)
|
||||
sig = T::Private::Methods.signature_for_method(method)
|
||||
sig ? sig.method.arity : arity
|
||||
end
|
||||
|
||||
# Elide the middle of a string as needed and replace it with an ellipsis.
|
||||
# Keep the given number of characters at the start and end of the string.
|
||||
#
|
||||
# This method operates on string length, not byte length.
|
||||
#
|
||||
# If the string is shorter than the requested truncation length, return it
|
||||
# without adding an ellipsis. This method may return a longer string than
|
||||
# the original if the characters removed are shorter than the ellipsis.
|
||||
#
|
||||
# @param [String] str
|
||||
#
|
||||
# @param [Fixnum] start_len The length of string before the ellipsis
|
||||
# @param [Fixnum] end_len The length of string after the ellipsis
|
||||
#
|
||||
# @param [String] ellipsis The string to add in place of the elided text
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
def self.string_truncate_middle(str, start_len, end_len, ellipsis='...')
|
||||
return unless str
|
||||
|
||||
raise ArgumentError.new('must provide start_len') unless start_len
|
||||
raise ArgumentError.new('must provide end_len') unless end_len
|
||||
|
||||
raise ArgumentError.new('start_len must be >= 0') if start_len < 0
|
||||
raise ArgumentError.new('end_len must be >= 0') if end_len < 0
|
||||
|
||||
str = str.to_s
|
||||
return str if str.length <= start_len + end_len
|
||||
|
||||
start_part = str[0...start_len - ellipsis.length]
|
||||
end_part = end_len == 0 ? '' : str[-end_len..-1]
|
||||
|
||||
"#{start_part}#{ellipsis}#{end_part}"
|
||||
end
|
||||
|
||||
def self.lift_enum(enum)
|
||||
unless enum.is_a?(T::Types::Enum)
|
||||
raise ArgumentError.new("#{enum.inspect} is not a T.deprecated_enum")
|
||||
end
|
||||
|
||||
classes = enum.values.map(&:class).uniq
|
||||
if classes.empty?
|
||||
T.untyped
|
||||
elsif classes.length > 1
|
||||
T::Types::Union.new(classes)
|
||||
else
|
||||
T::Types::Simple::Private::Pool.type_for_module(classes.first)
|
||||
end
|
||||
end
|
||||
|
||||
module Nilable
|
||||
# :is_union_type, T::Boolean: whether the type is an T::Types::Union type
|
||||
# :non_nilable_type, Class: if it is an T.nilable type, the corresponding underlying type; otherwise, nil.
|
||||
TypeInfo = Struct.new(:is_union_type, :non_nilable_type)
|
||||
|
||||
NIL_TYPE = T::Utils.coerce(NilClass)
|
||||
|
||||
def self.get_type_info(prop_type)
|
||||
if prop_type.is_a?(T::Types::Union)
|
||||
non_nilable_type = T::Utils.unwrap_nilable(prop_type)
|
||||
if non_nilable_type&.is_a?(T::Types::Simple)
|
||||
non_nilable_type = non_nilable_type.raw_type
|
||||
end
|
||||
TypeInfo.new(true, non_nilable_type)
|
||||
else
|
||||
TypeInfo.new(false, nil)
|
||||
end
|
||||
end
|
||||
|
||||
# Get the underlying type inside prop_type:
|
||||
# - if the type is A, the function returns A
|
||||
# - if the type is T.nilable(A), the function returns A
|
||||
def self.get_underlying_type(prop_type)
|
||||
type_info = get_type_info(prop_type)
|
||||
if type_info.is_union_type
|
||||
type_info.non_nilable_type || prop_type
|
||||
elsif prop_type.is_a?(T::Types::Simple)
|
||||
prop_type.raw_type
|
||||
else
|
||||
prop_type
|
||||
end
|
||||
end
|
||||
|
||||
# The difference between this function and the above function is that the Sorbet type, like T::Types::Simple
|
||||
# is preserved.
|
||||
def self.get_underlying_type_object(prop_type)
|
||||
T::Utils.unwrap_nilable(prop_type) || prop_type
|
||||
end
|
||||
|
||||
def self.is_union_with_nilclass(prop_type)
|
||||
case prop_type
|
||||
when T::Types::Union
|
||||
prop_type.types.include?(NIL_TYPE)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,146 +0,0 @@
|
||||
# typed: ignore
|
||||
|
||||
begin
|
||||
gem "sorbet-runtime"
|
||||
return
|
||||
rescue Gem::LoadError
|
||||
end
|
||||
|
||||
module T
|
||||
class << self
|
||||
def absurd(value); end
|
||||
def all(type_a, type_b, *types); end
|
||||
def any(type_a, type_b, *types); end
|
||||
def attached_class; end
|
||||
def class_of(klass); end
|
||||
def enum(values); end
|
||||
def nilable(type); end
|
||||
def noreturn; end
|
||||
def self_type; end
|
||||
def type_alias(type=nil, &_blk); end
|
||||
def type_parameter(name); end
|
||||
def untyped; end
|
||||
|
||||
def assert_type!(value, _type, _checked: true)
|
||||
value
|
||||
end
|
||||
|
||||
def cast(value, _type, _checked: true)
|
||||
value
|
||||
end
|
||||
|
||||
def let(value, _type, _checked: true)
|
||||
value
|
||||
end
|
||||
|
||||
def must(arg, _msg = nil)
|
||||
arg
|
||||
end
|
||||
|
||||
def proc
|
||||
T::Proc.new
|
||||
end
|
||||
|
||||
def reveal_type(value)
|
||||
value
|
||||
end
|
||||
|
||||
def unsafe(value)
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
module Sig
|
||||
def sig(arg0=nil, &blk); end
|
||||
end
|
||||
|
||||
module Helpers
|
||||
def abstract!; end
|
||||
def interface!; end
|
||||
def final!; end
|
||||
def sealed!; end
|
||||
def mixes_in_class_methods(mod); end
|
||||
end
|
||||
|
||||
module Generic
|
||||
include T::Helpers
|
||||
|
||||
def type_parameters(*params); end
|
||||
def type_member(variance=:invariant, fixed: nil, lower: nil, upper: BasicObject); end
|
||||
def type_template(variance=:invariant, fixed: nil, lower: nil, upper: BasicObject); end
|
||||
|
||||
def [](*types)
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
module Array
|
||||
def self.[](type); end
|
||||
end
|
||||
|
||||
Boolean = Object.new.freeze
|
||||
|
||||
module Configuration
|
||||
def self.call_validation_error_handler(signature, opts); end
|
||||
def self.call_validation_error_handler=(value); end
|
||||
def self.default_checked_level=(default_checked_level); end
|
||||
def self.enable_checking_for_sigs_marked_checked_tests; end
|
||||
def self.enable_final_checks_on_hooks; end
|
||||
def self.enable_legacy_t_enum_migration_mode; end
|
||||
def self.reset_final_checks_on_hooks; end
|
||||
def self.hard_assert_handler(str, extra); end
|
||||
def self.hard_assert_handler=(value); end
|
||||
def self.inline_type_error_handler(error); end
|
||||
def self.inline_type_error_handler=(value); end
|
||||
def self.log_info_handler(str, extra); end
|
||||
def self.log_info_handler=(value); end
|
||||
def self.scalar_types; end
|
||||
def self.scalar_types=(values); end
|
||||
def self.sealed_violation_whitelist; end
|
||||
def self.sealed_violation_whitelist=(sealed_violation_whitelist); end
|
||||
def self.sig_builder_error_handler(error, location); end
|
||||
def self.sig_builder_error_handler=(value); end
|
||||
def self.sig_validation_error_handler(error, opts); end
|
||||
def self.sig_validation_error_handler=(value); end
|
||||
def self.soft_assert_handler(str, extra); end
|
||||
def self.soft_assert_handler=(value); end
|
||||
end
|
||||
|
||||
module Enumerable
|
||||
def self.[](type); end
|
||||
end
|
||||
|
||||
module Enumerator
|
||||
def self.[](type); end
|
||||
end
|
||||
|
||||
module Hash
|
||||
def self.[](keys, values); end
|
||||
end
|
||||
|
||||
class Proc
|
||||
def bind(*_)
|
||||
self
|
||||
end
|
||||
|
||||
def params(*_param)
|
||||
self
|
||||
end
|
||||
|
||||
def void
|
||||
self
|
||||
end
|
||||
|
||||
def returns(_type)
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
module Range
|
||||
def self.[](type); end
|
||||
end
|
||||
|
||||
module Set
|
||||
def self.[](type); end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user