lib/yard_types/types.rb
module YardTypes
# @api private
module OrList #:nodoc:
def or_list(ary)
size = ary.size
ary.to_enum.with_index.inject('') do |acc, (s, index)|
acc << s.to_s
acc << ", " if index < size - 2
acc << ", or " if index == size - 2
acc
end
end
end
# A `TypeConstraint` specifies the set of acceptable types
# which can satisfy the constraint. Parsing any YARD type
# description will return a `TypeConstraint` instance.
#
# @see YardTypes.parse
class TypeConstraint
# @return [Array<Type>] the list of types that will satisfy this constraint
attr_reader :accepted_types
# @param types [Array<Type>] the list of acceptable types
def initialize(types)
@accepted_types = types
end
# @param i [Fixnum]
# @return [Type] the type at index `i`
# @todo deprecate this; remnant from original TDD'd API.
def [](i)
accepted_types[i]
end
# @return [Type] the first type
# @todo deprecate this; remnant from original TDD'd API.
def first
self[0]
end
# @param obj [Object] Any object.
# @return [Type, nil] the first type which matched `obj`,
# or `nil` if none.
def check(obj)
accepted_types.find { |t| t.check(obj) }
end
# @return [String] a YARD type string describing this set of
# types.
def to_s
accepted_types.map(&:to_s).join(', ')
end
end
# @abstract The base class for all supported types.
class Type
# @return [String] the YARD string naming this type
attr_accessor :name
# @todo This interface was just hacked into place while
# enhancing the parser to return {DuckType}, {KindType}, etc.
# @api private
def self.for(name)
case name
when /^#/
DuckType.new(name)
when *LiteralType.names
LiteralType.new(name)
else
KindType.new(name)
end
end
# @param name [String]
def initialize(name)
@name = name
end
# @return [String] a YARD type string describing this type.
def to_s
name
end
# @return [String] an English phrase describing this type.
def description
raise NotImplementedError
end
# @param obj [Object] Any object.
# @return [Boolean] whether the object is of this type.
# @raise [NotImplementedError] must be handled by the subclasses.
def check(obj)
raise NotImplementedError
end
end
# A {DuckType} constraint is specified as `#some_message`,
# and indicates that the object must respond to the method
# `some_message`.
class DuckType < Type
# @return [String] The method the object must respond to;
# this does not include the leading `#` character.
attr_reader :message
# @param name [String] The YARD identifier, eg `#some_message`.
def initialize(name)
@name = name
@message = name[1..-1]
end
# (see Type#description)
def description
"an object that responds to #{name}"
end
# @param (see Type#check)
# @return [Boolean] `true` if the object responds to `message`.
def check(obj)
obj.respond_to? message
end
end
# A {KindType} constraint is specified as `SomeModule` or
# `SomeClass`, and indicates that the object must be a kind of that
# module.
class KindType < Type
# Type checks a given object. Special consideration is given to
# the pseudo-class `Boolean`, which does not actually exist in Ruby,
# but is commonly used to mean `TrueClass, FalseClass`.
#
# @param (see Type#check)
# @return [Boolean] `true` if `obj.kind_of?(constant)`.
def check(obj)
if name == 'Boolean'
obj == true || obj == false
else
obj.kind_of? constant
end
end
# (see Type#description)
def description
name
end
# @return [Module] the constant specified by `name`.
# @raise [TypeError] if the constant is neither a module nor a class
# @raise [NameError] if the specified constant could not be loaded.
def constant
@constant ||=
begin
const = name.split('::').reduce(Object) { |namespace, inner_const|
namespace.const_get(inner_const)
}
unless const.kind_of?(Module)
raise TypeError, "class or module required; #{name} is a #{const.class}"
end
const
end
end
end
# A {LiteralType} constraint is specified by the name of one of YARD's
# supported "literals": `true`, `false`, `nil`, `void`, and `self`, and
# indicates that the object must be exactly one of those values.
#
# However, `void` and `self` have no particular meaning: `void` is typically
# used solely to specify that a method returns no meaningful types; and
# `self` is used to specify that a method returns its receiver, generally
# to indicate that calls can be chained. All values type check as valid
# objects for `void` and `self` literals.
class LiteralType < Type
# @return [Array<String>] the list of supported literal identifiers.
def self.names
@literal_names ||= %w(true false nil void self)
end
# (see Type#description)
def description
name
end
# @param (see Type#check)
# @return [Boolean] `true` if the object is exactly `true`, `false`, or
# `nil` (depending on the value of `name`); for `void` and `self`
# types, this method *always* returns `true`.
# @raise [NotImplementedError] if an unsupported literal name is to be
# tested against.
def check(obj)
case name
when 'true' then obj == true
when 'false' then obj == false
when 'nil' then obj == nil
when 'self', 'void' then true
else raise NotImplementedError, "Unsupported literal type: #{name.inspect}"
end
end
end
# A {CollectionType} is specified with the syntax `Kind<Some, #thing>`, and
# indicates that the object is a kind of `Kind`, containing only objects which
# type check against `Some` or `#thing`.
#
# @todo The current implementation of type checking here requires that the collection
# respond to `all?`; this may not be ideal.
class CollectionType < Type
include OrList
# @return [Array<Type>] the acceptable types for this collection's contents.
attr_accessor :types
# @param name [String] the name of the module the collection must be a kind of.
# @param types [Array<Type>] the acceptable types for the collection's contents.
def initialize(name, types)
@name = name
@types = types
end
# (see Type#to_s)
def to_s
"%s<%s>" % [name, types.map(&:to_s).join(', ')]
end
# (see Type#description)
def description
article = name[0] =~ /[aeiou]/i ? 'an' : 'a'
type_descriptions = types.map(&:description)
"#{article} #{name} of (#{or_list(type_descriptions)})"
end
# @param (see Type#check)
# @return [Boolean] `true` if the object is both a kind of `name`, and all of
# its contents (if any) are of the types in `types`. Any combination, order,
# and count of content types is acceptable.
def check(obj)
return false unless KindType.new(name).check(obj)
obj.all? do |el|
# TODO -- could probably just use another TypeConstraint here
types.any? { |type| type.check(el) }
end
end
end
# A {TupleType} is specified with the syntax `(Some, Types, #here)`, and indicates
# that the contents of the collection must be exactly that size, and each element
# must be of the exact type specified for that index.
#
# @todo The current implementation of type checking here requires that the collection
# respond to both `length` and `[]`; this may not be ideal.
class TupleType < CollectionType
def initialize(name, types)
@name = name == '<generic-tuple>' ? nil : name
@types = types
end
# (see Type#to_s)
def to_s
"%s(%s)" % [name, types.map(&:to_s).join(', ')]
end
# (see Type#description)
def description
kind = name || 'tuple'
article = kind[0] =~ /[aeiou]/i ? 'an' : 'a'
contents = types.map(&:description).join(', ')
"#{article} #{kind} containing (#{contents})"
end
# @param (see Type#check)
# @return [Boolean] `true` if the collection's `length` is exactly the length of
# the expected `types`, and each element with the collection is of the type
# specified for that index by `types`.
def check(obj)
return false unless name.nil? || KindType.new(name).check(obj)
return false unless obj.respond_to?(:length) && obj.respond_to?(:[])
return false unless obj.length == types.length
enum = types.to_enum
enum.with_index.all? do |t, i|
t.check(obj[i])
end
end
end
# A {HashType} is specified with the syntax `{KeyType => ValueType}`,
# and indicates that all keys in the hash must be of type
# `KeyType`, and all values must be of type `ValueType`.
#
# An alternate syntax for {HashType} is also available as `Hash<A, B>`,
# but its usage is not recommended; it is less capable than the
# `{A => B}` syntax, as some inner type constraints can not be
# parsed reliably.
#
# A {HashType} actually only requires that the object respond to
# both `keys` and `values`; it should be capable of type checking
# any object which conforms to that interface.
#
# @todo Enforce kind, eg `HashWithIndifferentAccess{#to_sym => Array}`,
# in case you _really_ care that it's indifferent. Maybe?
class HashType < Type
include OrList
# @return [Array<Type>] the set of acceptable types for keys
attr_reader :key_types
# @return [Array<Type>] the set of acceptable types for values
attr_reader :value_types
# @param name [String] the kind of the expected object; currently unused.
# @param key_types [Array<Type>] the set of acceptable types for keys
# @param value_types [Array<Type>] the set of acceptable types for values
def initialize(name, key_types, value_types)
@name = name
@key_types = key_types
@value_types = value_types
end
# Unlike the other types, {HashType} can result from two alternate syntaxes;
# however, this method will *only* return the `{A => B}` syntax.
#
# @return (see Type#to_s)
def to_s
"{%s => %s}" % [
key_types.map(&:to_s).join(', '),
value_types.map(&:to_s).join(', ')
]
end
# (see Type#description)
def description
article = name[0] =~ /[aeiou]/i ? 'an' : 'a'
key_descriptions = or_list(key_types.map(&:description))
value_descriptions = or_list(value_types.map(&:description))
"#{article} #{name} with keys of (#{key_descriptions}) and values of (#{value_descriptions})"
end
# @param (see Type#check)
# @return [Boolean] `true` if the object responds to both `keys` and `values`,
# and every key type checks against a type in `key_types`, and every value
# type checks against a type in `value_types`.
def check(obj)
return false unless obj.respond_to?(:keys) && obj.respond_to?(:values)
obj.keys.all? { |key| key_types.any? { |t| t.check(key) } } &&
obj.values.all? { |value| value_types.any? { |t| t.check(value) } }
end
end
end