module Sord::TypeConverter

Contains methods to convert YARD types to Parlour types.

Constants

DUCK_TYPE_REGEX

Match duck types which require the object implement one or more methods, like '#foo', '#foo & bar', '#foo&#bar&#baz', and '#foo&#bar&#baz&#foo_bar'.

GENERIC_TYPE_REGEX

A regular expression which matches a Ruby namespace immediately followed by another Ruby namespace in angle brackets or curly braces. This is the format usually used in YARD to model generic types, such as “Array<String>”, “Hash<String, Symbol>”, “Hash{String => Symbol}”, etc.

ORDERED_LIST_REGEX

A regular expression which matches ordered lists in the format of either “Array(String, Symbol)” or “(String, Symbol)”.

SHORTHAND_ARRAY_SYNTAX

A regular expression which matches the shorthand Array syntax, “<String>”.

SHORTHAND_HASH_SYNTAX

A regular expression which matches the shorthand Hash syntax, “{String => Symbol}”.

SIMPLE_TYPE_REGEX

A regular expression which matches Ruby namespaces and identifiers. “Foo”, “Foo::Bar”, and “::Foo::Bar” are all matches, whereas “Foo.Bar” or “Foo#bar” are not.

SINGLE_ARG_GENERIC_TYPES

Built in parlour single arg generics

Public Class Methods

handle_sord_error(name, log_warning, item, replace_errors_with_untyped) click to toggle source

Handles SORD_ERRORs.

@param [String, Parlour::Types::Type] name @param [String] log_warning @param [YARD::CodeObjects::Base] item @param [Boolean] replace_errors_with_untyped @return [Parlour::Types::Type]

# File lib/sord/type_converter.rb, line 229
def self.handle_sord_error(name, log_warning, item, replace_errors_with_untyped)
  Logging.warn(log_warning, item)
  str = name.is_a?(Parlour::Types::Type) ? name.describe : name
  return replace_errors_with_untyped \
    ? Parlour::Types::Untyped.new
    : Parlour::Types::Raw.new("SORD_ERROR_#{name.gsub(/[^0-9A-Za-z_]/i, '')}")
end
split_type_parameters(params) click to toggle source

Given a string of YARD type parameters (without angle brackets), splits the string into an array of each type parameter. @param [String] params The type parameters. @return [Array<String>] The split type parameters.

# File lib/sord/type_converter.rb, line 48
def self.split_type_parameters(params)
  result = []
  buffer = ""
  current_bracketing_level = 0
  character_pointer = 0

  while character_pointer < params.length
    should_buffer = true

    current_bracketing_level += 1 if ['<', '{', '('].include?(params[character_pointer])
    # Decrease bracketing level by 1 when encountering `>` or `}`, unless
    # the previous character is `=` (to prevent hash rockets from causing
    # nesting problems).
    current_bracketing_level -= 1 if ['>', '}', ')'].include?(params[character_pointer]) && params[character_pointer - 1] != '='

    # Handle commas as separators.
    # e.g. Hash<Symbol, String>
    if params[character_pointer] == ','
      if current_bracketing_level == 0
        result << buffer.strip
        buffer = ""
        should_buffer = false
      end
    end

    # Handle hash rockets as separators.
    # e.g. Hash<Symbol => String>
    if params[character_pointer] == '=' && params[character_pointer + 1] == '>'
      if current_bracketing_level == 0
        character_pointer += 1
        result << buffer.strip
        buffer = ""
        should_buffer = false
      end
    end

    buffer += params[character_pointer] if should_buffer
    character_pointer += 1
  end

  result << buffer.strip

  result
end
yard_to_parlour(yard, item = nil, replace_errors_with_untyped = false, replace_unresolved_with_untyped = false) click to toggle source

Converts a YARD type into a Parlour type. @param [Boolean, Array, String] yard The YARD type. @param [YARD::CodeObjects::Base] item The CodeObject which the YARD type

is associated with. This is used for logging and can be nil, but this
will lead to less informative log messages.

@param [Boolean] replace_errors_with_untyped If true, T.untyped is used

instead of SORD_ERROR_ constants for unknown types.

@param [Boolean] replace_unresolved_with_untyped If true, T.untyped is used

when Sord is unable to resolve a constant.

@return [Parlour::Types::Type]

# File lib/sord/type_converter.rb, line 103
def self.yard_to_parlour(yard, item = nil, replace_errors_with_untyped = false, replace_unresolved_with_untyped = false)
  case yard
  when nil # Type not specified
    Parlour::Types::Untyped.new
  when  "bool", "Bool", "boolean", "Boolean", "true", "false"
    Parlour::Types::Boolean.new
  when 'self'
    Parlour::Types::Self.new
  when Array
    # If there's only one element, unwrap it, otherwise allow for a
    # selection of any of the types
    types = yard
      .reject { |x| x == 'nil' }
      .map { |x| yard_to_parlour(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
      .uniq(&:hash)
    result = types.length == 1 \
      ? types.first
      : Parlour::Types::Union.new(types)
    result = Parlour::Types::Nilable.new(result) if yard.include?('nil')
    result
  when /^#{SIMPLE_TYPE_REGEX}$/
    if SINGLE_ARG_GENERIC_TYPES.include?(yard)
      return Parlour::Types.const_get(yard).new(Parlour::Types::Untyped.new)
    elsif yard == "Hash"
      return Parlour::Types::Hash.new(
        Parlour::Types::Untyped.new, Parlour::Types::Untyped.new
      )
    end
    # If this doesn't begin with an uppercase letter, warn
    if /^[_a-z]/ === yard
      Logging.warn("#{yard} is probably not a type, but using anyway", item)
    end

    # Check if whatever has been specified is actually resolvable; if not,
    # do some inference to replace it
    if item && !Resolver.resolvable?(yard, item)
      if Resolver.path_for(yard)
        new_path = Resolver.path_for(yard)
        Logging.infer("#{yard} was resolved to #{new_path}", item) \
          unless yard == new_path
        Parlour::Types::Raw.new(new_path)
      else
        if replace_unresolved_with_untyped
          Logging.warn("#{yard} wasn't able to be resolved to a constant in this project, replaced with untyped", item)
          Parlour::Types::Untyped.new
        else
          Logging.warn("#{yard} wasn't able to be resolved to a constant in this project", item)
          Parlour::Types::Raw.new(yard)
        end
      end
    else
      Parlour::Types::Raw.new(yard)
    end
  when DUCK_TYPE_REGEX
    Logging.duck("#{yard} looks like a duck type, replacing with untyped", item)
    Parlour::Types::Untyped.new
  when /^#{GENERIC_TYPE_REGEX}$/
    generic_type = $1
    type_parameters = $2

    parameters = split_type_parameters(type_parameters)
      .map { |x| yard_to_parlour(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
    if SINGLE_ARG_GENERIC_TYPES.include?(generic_type) && parameters.length > 1
      Parlour::Types.const_get(generic_type).new(Parlour::Types::Union.new(parameters))
    elsif generic_type == 'Class' && parameters.length == 1
      Parlour::Types::Class.new(parameters.first)
    elsif generic_type == 'Hash'
      if parameters.length == 2
        Parlour::Types::Hash.new(*parameters)
      else
        handle_sord_error(parameters.map(&:describe).join, "Invalid hash, must have exactly two types: #{yard.inspect}.", item, replace_errors_with_untyped)
      end
    else
      if Parlour::Types.const_defined?(generic_type)
        # This generic is built in to parlour, but sord doesn't
        # explicitly know about it.
        Parlour::Types.const_get(generic_type).new(*parameters)
      else
        # This is a user defined generic
        Parlour::Types::Generic.new(
          yard_to_parlour(generic_type),
          parameters
        )
      end
    end
  # Converts ordered lists like Array(Symbol, String) or (Symbol, String)
  # into tuples.
  when ORDERED_LIST_REGEX
    type_parameters = $1
    parameters = split_type_parameters(type_parameters)
      .map { |x| yard_to_parlour(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
    Parlour::Types::Tuple.new(parameters)
  when SHORTHAND_HASH_SYNTAX
    type_parameters = $1
    parameters = split_type_parameters(type_parameters)
      .map { |x| yard_to_parlour(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
    # Return a warning about an invalid hash when it has more or less than two elements.
    if parameters.length == 2
      Parlour::Types::Hash.new(*parameters)
    else
      handle_sord_error(parameters.map(&:describe).join, "Invalid hash, must have exactly two types: #{yard.inspect}.", item, replace_errors_with_untyped)
    end
  when SHORTHAND_ARRAY_SYNTAX
    type_parameters = $1
    parameters = split_type_parameters(type_parameters)
      .map { |x| yard_to_parlour(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
    parameters.one? \
      ? Parlour::Types::Array.new(parameters.first)
      : Parlour::Types::Array.new(Parlour::Types::Union.new(parameters))
  else
    # Check for literals
    from_yaml = YAML.load(yard) rescue nil
    return Parlour::Types::Raw.new(from_yaml.class.to_s) \
      if [Symbol, Float, Integer].include?(from_yaml.class)

    return handle_sord_error(yard.to_s, "#{yard.inspect} does not appear to be a type", item, replace_errors_with_untyped)
  end
end