sorbet-runtime 0.5.10439 → 0.5.11120

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sorbet-runtime.rb +7 -1
  3. data/lib/types/_types.rb +57 -3
  4. data/lib/types/compatibility_patches.rb +4 -2
  5. data/lib/types/enum.rb +6 -1
  6. data/lib/types/generic.rb +2 -0
  7. data/lib/types/private/abstract/declare.rb +10 -9
  8. data/lib/types/private/casts.rb +4 -1
  9. data/lib/types/private/class_utils.rb +13 -6
  10. data/lib/types/private/methods/_methods.rb +37 -12
  11. data/lib/types/private/methods/call_validation.rb +104 -5
  12. data/lib/types/private/methods/call_validation_2_6.rb +68 -60
  13. data/lib/types/private/methods/call_validation_2_7.rb +68 -60
  14. data/lib/types/private/methods/decl_builder.rb +21 -6
  15. data/lib/types/private/methods/signature.rb +63 -38
  16. data/lib/types/private/methods/signature_validation.rb +68 -6
  17. data/lib/types/private/runtime_levels.rb +19 -0
  18. data/lib/types/private/types/not_typed.rb +2 -0
  19. data/lib/types/private/types/simple_pair_union.rb +55 -0
  20. data/lib/types/private/types/void.rb +29 -23
  21. data/lib/types/props/_props.rb +2 -2
  22. data/lib/types/props/custom_type.rb +2 -2
  23. data/lib/types/props/decorator.rb +41 -36
  24. data/lib/types/props/has_lazily_specialized_methods.rb +2 -2
  25. data/lib/types/props/pretty_printable.rb +45 -83
  26. data/lib/types/props/private/setter_factory.rb +1 -1
  27. data/lib/types/props/serializable.rb +17 -9
  28. data/lib/types/props/type_validation.rb +5 -2
  29. data/lib/types/struct.rb +2 -2
  30. data/lib/types/types/anything.rb +31 -0
  31. data/lib/types/types/base.rb +15 -1
  32. data/lib/types/types/class_of.rb +11 -0
  33. data/lib/types/types/enum.rb +1 -1
  34. data/lib/types/types/fixed_array.rb +13 -0
  35. data/lib/types/types/fixed_hash.rb +22 -0
  36. data/lib/types/types/intersection.rb +1 -1
  37. data/lib/types/types/noreturn.rb +0 -1
  38. data/lib/types/types/simple.rb +27 -4
  39. data/lib/types/types/type_parameter.rb +19 -0
  40. data/lib/types/types/typed_array.rb +29 -0
  41. data/lib/types/types/typed_class.rb +85 -0
  42. data/lib/types/types/typed_enumerable.rb +7 -0
  43. data/lib/types/types/typed_enumerator_chain.rb +41 -0
  44. data/lib/types/types/union.rb +50 -14
  45. data/lib/types/utils.rb +41 -33
  46. metadata +14 -10
@@ -6,14 +6,63 @@ module T::Private::Methods::SignatureValidation
6
6
  Modes = Methods::Modes
7
7
 
8
8
  def self.validate(signature)
9
+ # Constructors in any language are always a bit weird: they're called in a
10
+ # static context, but their bodies are implemented by instance methods. So
11
+ # a mix of the rules that apply to instance methods and class methods
12
+ # apply.
13
+ #
14
+ # In languages like Java and Scala, static methods/companion object methods
15
+ # are never inherited. (In Java it almost looks like you can inherit them,
16
+ # because `Child.static_parent_method` works, but this method is simply
17
+ # resolved statically to `Parent.static_parent_method`). Even though most
18
+ # instance methods overrides have variance checking done, constructors are
19
+ # not treated like this, because static methods are never
20
+ # inherited/overridden, and the constructor can only ever be called
21
+ # indirectly by way of the static method. (Note: this is only a mental
22
+ # model--there's not actually a static method for the constructor in Java,
23
+ # there's an `invokespecial` JVM instruction that handles this).
24
+ #
25
+ # But Ruby is not like Java: singleton class methods in Ruby *are*
26
+ # inherited, unlike static methods in Java. In fact, this is similar to how
27
+ # JavaScript works. TypeScript simply then sidesteps the issue with
28
+ # structural typing: `typeof Parent` is not compatible with `typeof Child`
29
+ # if their constructors are different. (In a nominal type system, simply
30
+ # having Child descend from Parent should be the only factor in determining
31
+ # whether those types are compatible).
32
+ #
33
+ # Flow has nominal subtyping for classes. When overriding (static and
34
+ # instance) methods in a child class, the overrides must satisfy variance
35
+ # constraints. But it still carves out an exception for constructors,
36
+ # because then literally every class would have to have the same
37
+ # constructor. This is simply unsound. Hack does a similar thing--static
38
+ # method overrides are checked, but not constructors. Though what Hack
39
+ # *does* have is a way to opt into override checking for constructors with
40
+ # a special annotation.
41
+ #
42
+ # It turns out, Sorbet already has this special annotation: either
43
+ # `abstract` or `overridable`. At time of writing, *no* static override
44
+ # checking happens unless marked with these keywords (though at runtime, it
45
+ # always happens). Getting the static system to parity with the runtime by
46
+ # always checking overrides would be a great place to get to one day, but
47
+ # for now we can take advantage of it by only doing override checks for
48
+ # constructors if they've opted in.
49
+ #
50
+ # (When we get around to more widely checking overrides statically, we will
51
+ # need to build a matching special case for constructors statically.)
52
+ #
53
+ # Note that this breaks with tradition: normally, constructors are not
54
+ # allowed to be abstract. But that's kind of a side-effect of everything
55
+ # above: in Java/Scala, singleton class methods are never abstract because
56
+ # they're not inherited, and this extends to constructors. TypeScript
57
+ # simply rejects `new klass()` entirely if `klass` is
58
+ # `typeof AbstractClass`, requiring instead that you write
59
+ # `{ new(): AbstractClass }`. We may want to consider building some
60
+ # analogue to `T.class_of` in the future that works like this `{new():
61
+ # ...}` type.
9
62
  if signature.method_name == :initialize && signature.method.owner.is_a?(Class)
10
- # Constructors are special. They look like overrides in terms of a super_method existing,
11
- # but in practice, you never call them polymorphically. Conceptually, they're standard
12
- # methods (this is consistent with how they're treated in other languages, e.g. Java)
13
- if signature.mode != Modes.standard
14
- raise "`initialize` should not use `.abstract` or `.implementation` or any other inheritance modifiers."
63
+ if signature.mode == Modes.standard
64
+ return
15
65
  end
16
- return
17
66
  end
18
67
 
19
68
  super_method = signature.method.super_method
@@ -59,6 +108,19 @@ module T::Private::Methods::SignatureValidation
59
108
  case signature.mode
60
109
  when *Modes::OVERRIDE_MODES
61
110
  # Peaceful
111
+ when Modes.abstract
112
+ # Either the parent method is abstract, or it's not.
113
+ #
114
+ # If it's abstract, we want to allow overriding abstract with abstract to
115
+ # possibly narrow the type or provide more specific documentation.
116
+ #
117
+ # If it's not, then marking this method `abstract` will silently be a no-op.
118
+ # That's bad and we probably want to report an error, but fixing that
119
+ # will have to be a separate fix (that bad behavior predates this current
120
+ # comment, introduced when we fixed the abstract/abstract case).
121
+ #
122
+ # Therefore:
123
+ # Peaceful (mostly)
62
124
  when *Modes::NON_OVERRIDE_MODES
63
125
  if super_signature.mode == Modes.standard
64
126
  # Peaceful
@@ -53,10 +53,29 @@ module T::Private::RuntimeLevels
53
53
  if @has_read_default_checked_level
54
54
  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."
55
55
  end
56
+ if !LEVELS.include?(default_checked_level)
57
+ raise "Invalid `checked` level '#{default_checked_level}'. Use one of: #{LEVELS}."
58
+ end
59
+
56
60
  @default_checked_level = default_checked_level
57
61
  end
58
62
 
59
63
  def self._toggle_checking_tests(checked)
60
64
  @check_tests = checked
61
65
  end
66
+
67
+ private_class_method def self.set_enable_checking_in_tests_from_environment
68
+ if ENV['SORBET_RUNTIME_ENABLE_CHECKING_IN_TESTS']
69
+ enable_checking_in_tests
70
+ end
71
+ end
72
+ set_enable_checking_in_tests_from_environment
73
+
74
+ private_class_method def self.set_default_checked_level_from_environment
75
+ level = ENV['SORBET_RUNTIME_DEFAULT_CHECKED_LEVEL']
76
+ if level
77
+ self.default_checked_level = level.to_sym
78
+ end
79
+ end
80
+ set_default_checked_level_from_environment
62
81
  end
@@ -20,4 +20,6 @@ class T::Private::Types::NotTyped < T::Types::Base
20
20
  private def subtype_of_single?(other)
21
21
  raise ERROR_MESSAGE
22
22
  end
23
+
24
+ INSTANCE = ::T::Private::Types::NotTyped.new.freeze
23
25
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ # Specialization of Union for the common case of the union of two simple types.
5
+ #
6
+ # This covers e.g. T.nilable(SomeModule), T.any(Integer, Float), and T::Boolean.
7
+ class T::Private::Types::SimplePairUnion < T::Types::Union
8
+ class DuplicateType < RuntimeError; end
9
+
10
+ # @param type_a [T::Types::Simple]
11
+ # @param type_b [T::Types::Simple]
12
+ def initialize(type_a, type_b)
13
+ if type_a == type_b
14
+ raise DuplicateType.new("#{type_a} == #{type_b}")
15
+ end
16
+
17
+ @raw_a = type_a.raw_type
18
+ @raw_b = type_b.raw_type
19
+ end
20
+
21
+ # @override Union
22
+ def recursively_valid?(obj)
23
+ obj.is_a?(@raw_a) || obj.is_a?(@raw_b)
24
+ end
25
+
26
+ # @override Union
27
+ def valid?(obj)
28
+ obj.is_a?(@raw_a) || obj.is_a?(@raw_b)
29
+ end
30
+
31
+ # @override Union
32
+ def types
33
+ # We reconstruct the simple types rather than just storing them because
34
+ # (1) this is normally not a hot path and (2) we want to keep the instance
35
+ # variable count <= 3 so that we can fit in a 40 byte heap entry along
36
+ # with object headers.
37
+ @types ||= [
38
+ T::Types::Simple::Private::Pool.type_for_module(@raw_a),
39
+ T::Types::Simple::Private::Pool.type_for_module(@raw_b),
40
+ ]
41
+ end
42
+
43
+ # overrides Union
44
+ def unwrap_nilable
45
+ a_nil = @raw_a.equal?(NilClass)
46
+ b_nil = @raw_b.equal?(NilClass)
47
+ if a_nil
48
+ return types[1]
49
+ end
50
+ if b_nil
51
+ return types[0]
52
+ end
53
+ nil
54
+ end
55
+ end
@@ -3,32 +3,38 @@
3
3
 
4
4
  # A marking class for when methods return void.
5
5
  # Should never appear in types directly.
6
- class T::Private::Types::Void < T::Types::Base
7
- ERROR_MESSAGE = "Validation is being done on an `Void`. Please report this bug at https://github.com/sorbet/sorbet/issues"
6
+ module T::Private::Types
7
+ class Void < T::Types::Base
8
+ ERROR_MESSAGE = "Validation is being done on an `Void`. Please report this bug at https://github.com/sorbet/sorbet/issues"
8
9
 
9
- # The actual return value of `.void` methods.
10
- #
11
- # Uses `module VOID` because this gives it a readable name when someone
12
- # examines it in Pry or with `#inspect` like:
13
- #
14
- # T::Private::Types::Void::VOID
15
- #
16
- module VOID
17
- freeze
18
- end
10
+ # The actual return value of `.void` methods.
11
+ #
12
+ # Uses `module VOID` because this gives it a readable name when someone
13
+ # examines it in Pry or with `#inspect` like:
14
+ #
15
+ # T::Private::Types::Void::VOID
16
+ #
17
+ module VOID
18
+ freeze
19
+ end
19
20
 
20
- # overrides Base
21
- def name
22
- "<VOID>"
23
- end
21
+ # overrides Base
22
+ def name
23
+ "<VOID>"
24
+ end
24
25
 
25
- # overrides Base
26
- def valid?(obj)
27
- raise ERROR_MESSAGE
28
- end
26
+ # overrides Base
27
+ def valid?(obj)
28
+ raise ERROR_MESSAGE
29
+ end
30
+
31
+ # overrides Base
32
+ private def subtype_of_single?(other)
33
+ raise ERROR_MESSAGE
34
+ end
29
35
 
30
- # overrides Base
31
- private def subtype_of_single?(other)
32
- raise ERROR_MESSAGE
36
+ module Private
37
+ INSTANCE = Void.new.freeze
38
+ end
33
39
  end
34
40
  end
@@ -139,9 +139,9 @@ module T::Props
139
139
  end
140
140
 
141
141
  if cls_or_args.is_a?(Hash)
142
- self.prop(name, cls_or_args.merge(immutable: true))
142
+ self.prop(name, **cls_or_args.merge(immutable: true))
143
143
  else
144
- self.prop(name, cls_or_args, args.merge(immutable: true))
144
+ self.prop(name, cls_or_args, **args.merge(immutable: true))
145
145
  end
146
146
  end
147
147
 
@@ -57,12 +57,12 @@ module T::Props
57
57
  raise 'Please use "extend", not "include" to attach this module'
58
58
  end
59
59
 
60
- sig(:final) {params(val: Object).returns(T::Boolean).checked(:never)}
60
+ sig(:final) {params(val: T.untyped).returns(T::Boolean).checked(:never)}
61
61
  def self.scalar_type?(val)
62
62
  # We don't need to check for val's included modules in
63
63
  # T::Configuration.scalar_types, because T::Configuration.scalar_types
64
64
  # are all classes.
65
- klass = T.let(val.class, T.nilable(Class))
65
+ klass = val.class
66
66
  until klass.nil?
67
67
  return true if T::Configuration.scalar_types.include?(klass.to_s)
68
68
  klass = klass.superclass
@@ -17,7 +17,7 @@ class T::Props::Decorator
17
17
 
18
18
  class NoRulesError < StandardError; end
19
19
 
20
- EMPTY_PROPS = T.let({}.freeze, T::Hash[Symbol, Rules])
20
+ EMPTY_PROPS = T.let({}.freeze, T::Hash[Symbol, Rules], checked: false)
21
21
  private_constant :EMPTY_PROPS
22
22
 
23
23
  sig {params(klass: T.untyped).void.checked(:never)}
@@ -26,7 +26,7 @@ class T::Props::Decorator
26
26
  @class.plugins.each do |mod|
27
27
  T::Props::Plugin::Private.apply_decorator_methods(mod, self)
28
28
  end
29
- @props = T.let(EMPTY_PROPS, T::Hash[Symbol, Rules])
29
+ @props = T.let(EMPTY_PROPS, T::Hash[Symbol, Rules], checked: false)
30
30
  end
31
31
 
32
32
  # checked(:never) - O(prop accesses)
@@ -50,9 +50,9 @@ class T::Props::Decorator
50
50
  override = rules.delete(:override)
51
51
 
52
52
  if props.include?(prop) && !override
53
- raise ArgumentError.new("Attempted to redefine prop #{prop.inspect} that's already defined without specifying :override => true: #{prop_rules(prop)}")
53
+ raise ArgumentError.new("Attempted to redefine prop #{prop.inspect} on class #{@class} that's already defined without specifying :override => true: #{prop_rules(prop)}")
54
54
  elsif !props.include?(prop) && override
55
- raise ArgumentError.new("Attempted to override a prop #{prop.inspect} that doesn't already exist")
55
+ raise ArgumentError.new("Attempted to override a prop #{prop.inspect} on class #{@class} that doesn't already exist")
56
56
  end
57
57
 
58
58
  @props = @props.merge(prop => rules.freeze).freeze
@@ -79,7 +79,7 @@ class T::Props::Decorator
79
79
  extra
80
80
  setter_validate
81
81
  _tnilable
82
- ].map {|k| [k, true]}.to_h.freeze, T::Hash[Symbol, T::Boolean])
82
+ ].to_h {|k| [k, true]}.freeze, T::Hash[Symbol, T::Boolean], checked: false)
83
83
  private_constant :VALID_RULE_KEYS
84
84
 
85
85
  sig {params(key: Symbol).returns(T::Boolean).checked(:never)}
@@ -205,7 +205,7 @@ class T::Props::Decorator
205
205
  end
206
206
 
207
207
  # TODO: we should really be checking all the methods on `cls`, not just Object
208
- BANNED_METHOD_NAMES = T.let(Object.instance_methods.each_with_object({}) {|x, acc| acc[x] = true}.freeze, T::Hash[Symbol, TrueClass])
208
+ BANNED_METHOD_NAMES = T.let(Object.instance_methods.each_with_object({}) {|x, acc| acc[x] = true}.freeze, T::Hash[Symbol, TrueClass], checked: false)
209
209
 
210
210
  # checked(:never) - Rules hash is expensive to check
211
211
  sig do
@@ -247,10 +247,10 @@ class T::Props::Decorator
247
247
  nil
248
248
  end
249
249
 
250
- SAFE_NAME = T.let(/\A[A-Za-z_][A-Za-z0-9_-]*\z/.freeze, Regexp)
250
+ SAFE_NAME = T.let(/\A[A-Za-z_][A-Za-z0-9_-]*\z/.freeze, Regexp, checked: false)
251
251
 
252
252
  # Used to validate both prop names and serialized forms
253
- sig {params(name: T.any(Symbol, String)).void}
253
+ sig {params(name: T.any(Symbol, String)).void.checked(:never)}
254
254
  private def validate_prop_name(name)
255
255
  if !name.match?(SAFE_NAME)
256
256
  raise ArgumentError.new("Invalid prop name in #{@class.name}: #{name}")
@@ -258,7 +258,7 @@ class T::Props::Decorator
258
258
  end
259
259
 
260
260
  # This converts the type from a T::Type to a regular old ruby class.
261
- sig {params(type: T::Types::Base).returns(Module)}
261
+ sig {params(type: T::Types::Base).returns(Module).checked(:never)}
262
262
  private def convert_type_to_class(type)
263
263
  case type
264
264
  when T::Types::TypedArray, T::Types::FixedArray
@@ -300,7 +300,9 @@ class T::Props::Decorator
300
300
  .checked(:never)
301
301
  end
302
302
  private def prop_nilable?(cls, rules)
303
- T::Utils::Nilable.is_union_with_nilclass(cls) || (cls == T.untyped && rules.key?(:default) && rules[:default].nil?)
303
+ # NB: `prop` and `const` do not `T::Utils::coerce the type of the prop if it is a `Module`,
304
+ # hence the bare `NilClass` check.
305
+ T::Utils::Nilable.is_union_with_nilclass(cls) || ((cls == T.untyped || cls == NilClass) && rules.key?(:default) && rules[:default].nil?)
304
306
  end
305
307
 
306
308
  # checked(:never) - Rules hash is expensive to check
@@ -336,34 +338,34 @@ class T::Props::Decorator
336
338
  # Retrive the possible underlying object with T.nilable.
337
339
  type = T::Utils::Nilable.get_underlying_type(type)
338
340
 
339
- sensitivity_and_pii = {sensitivity: rules[:sensitivity]}
340
- normalize = T::Configuration.normalize_sensitivity_and_pii_handler
341
- if normalize
342
- sensitivity_and_pii = normalize.call(sensitivity_and_pii)
343
-
344
- # We check for Class so this is only applied on concrete
345
- # documents/models; We allow mixins containing props to not
346
- # specify their PII nature, as long as every class into which they
347
- # are ultimately included does.
348
- #
349
- if sensitivity_and_pii[:pii] && @class.is_a?(Class) && !T.unsafe(@class).contains_pii?
350
- raise ArgumentError.new(
351
- 'Cannot include a pii prop in a class that declares `contains_no_pii`'
352
- )
341
+ rules_sensitivity = rules[:sensitivity]
342
+ sensitivity_and_pii = {sensitivity: rules_sensitivity}
343
+ if !rules_sensitivity.nil?
344
+ normalize = T::Configuration.normalize_sensitivity_and_pii_handler
345
+ if normalize
346
+ sensitivity_and_pii = normalize.call(sensitivity_and_pii)
347
+
348
+ # We check for Class so this is only applied on concrete
349
+ # documents/models; We allow mixins containing props to not
350
+ # specify their PII nature, as long as every class into which they
351
+ # are ultimately included does.
352
+ #
353
+ if sensitivity_and_pii[:pii] && @class.is_a?(Class) && !T.unsafe(@class).contains_pii?
354
+ raise ArgumentError.new(
355
+ 'Cannot include a pii prop in a class that declares `contains_no_pii`'
356
+ )
357
+ end
353
358
  end
354
359
  end
355
360
 
356
- rules = rules.merge(
357
- # TODO: The type of this element is confusing. We should refactor so that
358
- # it can be always `type_object` (a PropType) or always `cls` (a Module)
359
- type: type,
360
- type_object: type_object,
361
- accessor_key: "@#{name}".to_sym,
362
- sensitivity: sensitivity_and_pii[:sensitivity],
363
- pii: sensitivity_and_pii[:pii],
364
- # extra arbitrary metadata attached by the code defining this property
365
- extra: rules[:extra]&.freeze,
366
- )
361
+ rules[:type] = type
362
+ rules[:type_object] = type_object
363
+ rules[:accessor_key] = "@#{name}".to_sym
364
+ rules[:sensitivity] = sensitivity_and_pii[:sensitivity]
365
+ rules[:pii] = sensitivity_and_pii[:pii]
366
+ rules[:extra] = rules[:extra]&.freeze
367
+
368
+ # extra arbitrary metadata attached by the code defining this property
367
369
 
368
370
  validate_not_missing_sensitivity(name, rules)
369
371
 
@@ -417,6 +419,7 @@ class T::Props::Decorator
417
419
  sig do
418
420
  params(type: PropTypeOrClass, enum: T.untyped)
419
421
  .returns(T::Types::Base)
422
+ .checked(:never)
420
423
  end
421
424
  private def smart_coerce(type, enum:)
422
425
  # Backwards compatibility for pre-T::Types style
@@ -469,6 +472,7 @@ class T::Props::Decorator
469
472
  redaction: T.untyped,
470
473
  )
471
474
  .void
475
+ .checked(:never)
472
476
  end
473
477
  private def handle_redaction_option(prop_name, redaction)
474
478
  redacted_method = "#{prop_name}_redacted"
@@ -490,6 +494,7 @@ class T::Props::Decorator
490
494
  valid_type_msg: String,
491
495
  )
492
496
  .void
497
+ .checked(:never)
493
498
  end
494
499
  private def validate_foreign_option(option_sym, foreign, valid_type_msg:)
495
500
  if foreign.is_a?(Symbol) || foreign.is_a?(String)
@@ -521,8 +526,8 @@ class T::Props::Decorator
521
526
  # here, but we're baking in `allow_direct_mutation` since we
522
527
  # *haven't* allowed additional options in the past and want to
523
528
  # default to keeping this interface narrow.
529
+ foreign = T.let(foreign, T.untyped, checked: false)
524
530
  @class.send(:define_method, fk_method) do |allow_direct_mutation: nil|
525
- foreign = T.let(foreign, T.untyped)
526
531
  if foreign.is_a?(Proc)
527
532
  resolved_foreign = foreign.call
528
533
  if !resolved_foreign.respond_to?(:load)
@@ -83,7 +83,7 @@ module T::Props
83
83
  lazily_defined_methods[name] = blk
84
84
 
85
85
  cls = decorated_class
86
- if cls.method_defined?(name)
86
+ if cls.method_defined?(name) || cls.private_method_defined?(name)
87
87
  # Ruby does not emit "method redefined" warnings for aliased methods
88
88
  # (more robust than undef_method that would create a small window in which the method doesn't exist)
89
89
  cls.send(:alias_method, name, name)
@@ -117,7 +117,7 @@ module T::Props
117
117
  def eagerly_define_lazy_methods!
118
118
  return if lazily_defined_methods.empty?
119
119
 
120
- source = lazily_defined_methods.values.map(&:call).map(&:to_s).join("\n\n")
120
+ source = "# frozen_string_literal: true\n" + lazily_defined_methods.values.map(&:call).map(&:to_s).join("\n\n")
121
121
 
122
122
  cls = decorated_class
123
123
  cls.class_eval(source)
@@ -1,18 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
  # typed: true
3
+ require 'pp'
3
4
 
4
5
  module T::Props::PrettyPrintable
5
6
  include T::Props::Plugin
6
7
 
7
- # Return a string representation of this object and all of its props
8
+ # Override the PP gem with something that's similar, but gives us a hook to do redaction and customization
9
+ def pretty_print(pp)
10
+ clazz = T.unsafe(T.cast(self, Object).class).decorator
11
+ multiline = pp.is_a?(PP)
12
+ pp.group(1, "<#{clazz.inspect_class_with_decoration(self)}", ">") do
13
+ clazz.all_props.sort.each do |prop|
14
+ pp.breakable
15
+ val = clazz.get(self, prop)
16
+ rules = clazz.prop_rules(prop)
17
+ pp.text("#{prop}=")
18
+ if (custom_inspect = rules[:inspect])
19
+ inspected = if T::Utils.arity(custom_inspect) == 1
20
+ custom_inspect.call(val)
21
+ else
22
+ custom_inspect.call(val, {multiline: multiline})
23
+ end
24
+ pp.text(inspected.nil? ? "nil" : inspected)
25
+ elsif rules[:sensitivity] && !rules[:sensitivity].empty? && !val.nil?
26
+ pp.text("<REDACTED #{rules[:sensitivity].join(', ')}>")
27
+ else
28
+ val.pretty_print(pp)
29
+ end
30
+ end
31
+ clazz.pretty_print_extra(self, pp)
32
+ end
33
+ end
34
+
35
+ # Return a string representation of this object and all of its props in a single line
8
36
  def inspect
9
- T.unsafe(T.cast(self, Object).class).decorator.inspect_instance(self)
37
+ string = +""
38
+ PP.singleline_pp(self, string)
39
+ string
10
40
  end
11
41
 
12
- # Override the PP gem with something that's similar, but gives us a hook
13
- # to do redaction
42
+ # Return a pretty string representation of this object and all of its props
14
43
  def pretty_inspect
15
- T.unsafe(T.cast(self, Object).class).decorator.inspect_instance(self, multiline: true)
44
+ string = +""
45
+ PP.pp(self, string)
46
+ string
16
47
  end
17
48
 
18
49
  module DecoratorMethods
@@ -23,85 +54,16 @@ module T::Props::PrettyPrintable
23
54
  super || key == :inspect
24
55
  end
25
56
 
26
- sig do
27
- params(instance: T::Props::PrettyPrintable, multiline: T::Boolean, indent: String)
28
- .returns(String)
57
+ # Overridable method to specify how the first part of a `pretty_print`d object's class should look like
58
+ # NOTE: This is just to support Stripe's `PrettyPrintableModel` case, and not recommended to be overriden
59
+ sig {params(instance: T::Props::PrettyPrintable).returns(String)}
60
+ def inspect_class_with_decoration(instance)
61
+ T.unsafe(instance).class.to_s
29
62
  end
30
- def inspect_instance(instance, multiline: false, indent: ' ')
31
- components =
32
- inspect_instance_components(
33
- instance,
34
- multiline: multiline,
35
- indent: indent
36
- )
37
- .reject(&:empty?)
38
-
39
- # Not using #<> here as that makes pry highlight these objects
40
- # as if they were all comments, whereas this makes them look
41
- # like the structured thing they are.
42
- if multiline
43
- "#{components[0]}:\n" + T.must(components[1..-1]).join("\n")
44
- else
45
- "<#{components.join(' ')}>"
46
- end
47
- end
48
-
49
- sig do
50
- params(instance: T::Props::PrettyPrintable, multiline: T::Boolean, indent: String)
51
- .returns(T::Array[String])
52
- end
53
- private def inspect_instance_components(instance, multiline:, indent:)
54
- pretty_props = T.unsafe(self).all_props.map do |prop|
55
- [prop, inspect_prop_value(instance, prop, multiline: multiline, indent: indent)]
56
- end
57
-
58
- joined_props = join_props_with_pretty_values(
59
- pretty_props,
60
- multiline: multiline,
61
- indent: indent
62
- )
63
63
 
64
- [
65
- T.unsafe(self).decorated_class.to_s,
66
- joined_props,
67
- ]
68
- end
69
-
70
- sig do
71
- params(instance: T::Props::PrettyPrintable, prop: Symbol, multiline: T::Boolean, indent: String)
72
- .returns(String)
73
- .checked(:never)
74
- end
75
- private def inspect_prop_value(instance, prop, multiline:, indent:)
76
- val = T.unsafe(self).get(instance, prop)
77
- rules = T.unsafe(self).prop_rules(prop)
78
- if (custom_inspect = rules[:inspect])
79
- if T::Utils.arity(custom_inspect) == 1
80
- custom_inspect.call(val)
81
- else
82
- custom_inspect.call(val, {multiline: multiline, indent: indent})
83
- end
84
- elsif rules[:sensitivity] && !rules[:sensitivity].empty? && !val.nil?
85
- "<REDACTED #{rules[:sensitivity].join(', ')}>"
86
- else
87
- val.inspect
88
- end
89
- end
90
-
91
- sig do
92
- params(pretty_kvs: T::Array[[Symbol, String]], multiline: T::Boolean, indent: String)
93
- .returns(String)
94
- end
95
- private def join_props_with_pretty_values(pretty_kvs, multiline:, indent: ' ')
96
- pairs = pretty_kvs
97
- .sort_by {|k, _v| k.to_s}
98
- .map {|k, v| "#{k}=#{v}"}
99
-
100
- if multiline
101
- indent + pairs.join("\n#{indent}")
102
- else
103
- pairs.join(', ')
104
- end
105
- end
64
+ # Overridable method to add anything that is not a prop
65
+ # NOTE: This is to support cases like Serializable's `@_extra_props`, and Stripe's `PrettyPrintableModel#@_deleted`
66
+ sig {params(instance: T::Props::PrettyPrintable, pp: T.any(PrettyPrint, PP::SingleLine)).void}
67
+ def pretty_print_extra(instance, pp); end
106
68
  end
107
69
  end
@@ -176,7 +176,7 @@ module T::Props
176
176
  base_message = "Can't set #{klass.name}.#{prop} to #{val.inspect} (instance of #{val.class}) - need a #{type}"
177
177
 
178
178
  pretty_message = "Parameter '#{prop}': #{base_message}\n"
179
- caller_loc = caller_locations&.find {|l| !l.to_s.include?('sorbet-runtime/lib/types/props')}
179
+ caller_loc = caller_locations.find {|l| !l.to_s.include?('sorbet-runtime/lib/types/props')}
180
180
  if caller_loc
181
181
  pretty_message += "Caller: #{caller_loc.path}:#{caller_loc.lineno}\n"
182
182
  end
@@ -223,9 +223,10 @@ module T::Props::Serializable::DecoratorMethods
223
223
  end
224
224
 
225
225
  def add_prop_definition(prop, rules)
226
- rules[:serialized_form] = rules.fetch(:name, prop.to_s)
226
+ serialized_form = rules.fetch(:name, prop.to_s)
227
+ rules[:serialized_form] = serialized_form
227
228
  res = super
228
- prop_by_serialized_forms[rules[:serialized_form]] = prop
229
+ prop_by_serialized_forms[serialized_form] = prop
229
230
  if T::Configuration.use_vm_prop_serde?
230
231
  enqueue_lazy_vm_method_definition!(:__t_props_generated_serialize) {generate_serialize2}
231
232
  enqueue_lazy_vm_method_definition!(:__t_props_generated_deserialize) {generate_deserialize2}
@@ -338,14 +339,21 @@ module T::Props::Serializable::DecoratorMethods
338
339
  end
339
340
  end
340
341
 
341
- # overrides T::Props::PrettyPrintable
342
- private def inspect_instance_components(instance, multiline:, indent:)
342
+ # adds to the default result of T::Props::PrettyPrintable
343
+ def pretty_print_extra(instance, pp)
344
+ # This is to maintain backwards compatibility with Stripe's codebase, where only the single line (through `inspect`)
345
+ # version is expected to add anything extra
346
+ return if !pp.is_a?(PP::SingleLine)
343
347
  if (extra_props = extra_props(instance)) && !extra_props.empty?
344
- pretty_kvs = extra_props.map {|k, v| [k.to_sym, v.inspect]}
345
- extra = join_props_with_pretty_values(pretty_kvs, multiline: false)
346
- super + ["@_extra_props=<#{extra}>"]
347
- else
348
- super
348
+ pp.breakable
349
+ pp.text("@_extra_props=")
350
+ pp.group(1, "<", ">") do
351
+ extra_props.each_with_index do |(prop, value), i|
352
+ pp.breakable unless i.zero?
353
+ pp.text("#{prop}=")
354
+ value.pretty_print(pp)
355
+ end
356
+ end
349
357
  end
350
358
  end
351
359
  end