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-*/
|
||||||
**/vendor/bundle/ruby/*/gems/simplecov-html-*/
|
**/vendor/bundle/ruby/*/gems/simplecov-html-*/
|
||||||
**/vendor/bundle/ruby/*/gems/sorbet-*/
|
**/vendor/bundle/ruby/*/gems/sorbet-*/
|
||||||
**/vendor/bundle/ruby/*/gems/sorbet-runtime-*/
|
!**/vendor/bundle/ruby/*/gems/sorbet-runtime-*/
|
||||||
!**/vendor/bundle/ruby/*/gems/sorbet-runtime-stub-*/
|
|
||||||
**/vendor/bundle/ruby/*/gems/spoom-*/
|
**/vendor/bundle/ruby/*/gems/spoom-*/
|
||||||
**/vendor/bundle/ruby/*/gems/stackprof-*/
|
**/vendor/bundle/ruby/*/gems/stackprof-*/
|
||||||
**/vendor/bundle/ruby/*/gems/strscan-*/
|
**/vendor/bundle/ruby/*/gems/strscan-*/
|
||||||
|
@ -17,6 +17,7 @@ gem "rspec-github", require: false
|
|||||||
gem "rspec-its", require: false
|
gem "rspec-its", require: false
|
||||||
gem "rspec_junit_formatter", require: false
|
gem "rspec_junit_formatter", require: false
|
||||||
gem "rspec-retry", require: false
|
gem "rspec-retry", require: false
|
||||||
|
gem "rspec-sorbet", require: false
|
||||||
gem "rspec-wait", require: false
|
gem "rspec-wait", require: false
|
||||||
gem "rubocop", require: false
|
gem "rubocop", require: false
|
||||||
gem "rubocop-ast", require: false
|
gem "rubocop-ast", require: false
|
||||||
@ -26,7 +27,6 @@ gem "warning", require: false
|
|||||||
|
|
||||||
group :sorbet, optional: true do
|
group :sorbet, optional: true do
|
||||||
gem "parlour", require: false
|
gem "parlour", require: false
|
||||||
gem "rspec-sorbet", require: false
|
|
||||||
gem "sorbet-static-and-runtime", require: false
|
gem "sorbet-static-and-runtime", require: false
|
||||||
gem "tapioca", require: false
|
gem "tapioca", require: false
|
||||||
end
|
end
|
||||||
@ -44,4 +44,4 @@ gem "rubocop-rails"
|
|||||||
gem "rubocop-rspec"
|
gem "rubocop-rspec"
|
||||||
gem "rubocop-sorbet"
|
gem "rubocop-sorbet"
|
||||||
gem "ruby-macho"
|
gem "ruby-macho"
|
||||||
gem "sorbet-runtime-stub"
|
gem "sorbet-runtime"
|
||||||
|
@ -162,7 +162,6 @@ GEM
|
|||||||
sorbet (0.5.10160)
|
sorbet (0.5.10160)
|
||||||
sorbet-static (= 0.5.10160)
|
sorbet-static (= 0.5.10160)
|
||||||
sorbet-runtime (0.5.10160)
|
sorbet-runtime (0.5.10160)
|
||||||
sorbet-runtime-stub (0.2.0)
|
|
||||||
sorbet-static (0.5.10160-universal-darwin-14)
|
sorbet-static (0.5.10160-universal-darwin-14)
|
||||||
sorbet-static-and-runtime (0.5.10160)
|
sorbet-static-and-runtime (0.5.10160)
|
||||||
sorbet (= 0.5.10160)
|
sorbet (= 0.5.10160)
|
||||||
@ -235,7 +234,7 @@ DEPENDENCIES
|
|||||||
ruby-macho
|
ruby-macho
|
||||||
simplecov
|
simplecov
|
||||||
simplecov-cobertura
|
simplecov-cobertura
|
||||||
sorbet-runtime-stub
|
sorbet-runtime
|
||||||
sorbet-static-and-runtime
|
sorbet-static-and-runtime
|
||||||
tapioca
|
tapioca
|
||||||
warning
|
warning
|
||||||
|
@ -88,7 +88,7 @@ module Homebrew
|
|||||||
def tests
|
def tests
|
||||||
args = tests_args.parse
|
args = tests_args.parse
|
||||||
|
|
||||||
Homebrew.install_bundler_gems!(groups: ["sorbet"])
|
Homebrew.install_bundler_gems!
|
||||||
|
|
||||||
require "byebug" if args.byebug?
|
require "byebug" if args.byebug?
|
||||||
|
|
||||||
|
@ -1,11 +1,47 @@
|
|||||||
# typed: true
|
# typed: true
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Explicitly prevent `sorbet-runtime` from being loaded.
|
require "sorbet-runtime"
|
||||||
def gem(name, *)
|
|
||||||
raise Gem::LoadError if name == "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
|
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_relative "startup/ruby_path"
|
||||||
require "startup/config"
|
require "startup/config"
|
||||||
require_relative "startup/bootsnap"
|
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/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-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-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/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/thor-1.2.1/lib"
|
||||||
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/spoom-1.1.11/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