Module: Sord::TypeConverter

Defined in:
lib/sord/type_converter.rb

Overview

Contains methods to convert YARD types to Parlour types.

Defined Under Namespace

Classes: Configuration

Constant Summary collapse

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.

/(?:\:\:)?[a-zA-Z_][\w]*(?:\:\:[a-zA-Z_][\w]*)*/
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=> Symbol”, etc.

/(#{SIMPLE_TYPE_REGEX})\s*[<{]\s*(.*)\s*[>}]/
METHOD_NAME_REGEX =

Matches valid method names. From: stackoverflow.com/a/4379197/2626000

/(?:[a-z_]\w*[?!=]?|\[\]=?|<<|>>|\*\*|[!~+\*\/%&^|-]|[<>]=?|<=>|={2,3}|![=~]|=~)/i
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’.

/^\##{METHOD_NAME_REGEX}(?:\s*\&\s*\##{METHOD_NAME_REGEX})*$/
ORDERED_LIST_REGEX =

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

/^(?:Array|)\((.*)\s*\)$/
SHORTHAND_HASH_SYNTAX =

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

/^{\s*(.*)\s*}$/
SHORTHAND_ARRAY_SYNTAX =

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

/^<\s*(.*)\s*>$/
SINGLE_ARG_GENERIC_TYPES =

Built in parlour single arg generics

%w{Array Set Enumerable Enumerator Range}
DUCK_TYPES_TO_RBS_TYPE_NAMES =

Taken from: github.com/ruby/rbs/blob/master/core/builtin.rbs When the latest commit was: 6c847d1

Interfaces which use generic arguments have those arguments as ‘untyped`, since I’m not aware of any standard way that these are specified.

{
  # Concrete
  "#to_i" => "_ToI",
  "#to_int" => "_ToInt",
  "#to_r" => "_ToR",
  "#to_s" => "_ToS",
  "#to_str" => "_ToStr",
  "#to_proc" => "_ToProc",
  "#to_path" => "_ToPath",
  "#read" => "_Reader",
  "#readpartial" => "_ReaderPartial",
  "#write" => "_Writer",
  "#rewind" => "_Rewindable",
  "#to_io" => "_ToIO",
  "#exception" => "_Exception",

  # Generic - these will be put in a `Types::Raw`, so writing RBS syntax is a little devious,
  # but by their nature we know they'll only be used in an RBS file, so it's probably fine
  "#to_hash" => "_ToHash[untyped, untyped]",
  "#each" => "_Each[untyped]",
}

Class Method Summary collapse

Class Method Details

.duck_type_to_rbs_type(type) ⇒ Parlour::Types::Type?

Given a YARD duck type string, attempts to convert it to one of a list of pre-defined RBS built-in interfaces.

For example, the common duck type ‘#to_s` has a built-in RBS equivalent `_ToS`.

If no such interface exists, returns ‘nil`.

Parameters:

  • type (String)

Returns:

  • (Parlour::Types::Type, nil)


306
307
308
309
310
311
312
313
# File 'lib/sord/type_converter.rb', line 306

def self.duck_type_to_rbs_type(type)
  type_name = DUCK_TYPES_TO_RBS_TYPE_NAMES[type]
  if !type_name.nil?
    Parlour::Types::Raw.new(type_name)
  else
    nil
  end
end

.handle_sord_error(name, log_warning, item, replace_errors_with_untyped) ⇒ Parlour::Types::Type

Handles SORD_ERRORs.

Parameters:

  • name (String, Parlour::Types::Type)
  • log_warning (String)
  • item (YARD::CodeObjects::Base)
  • replace_errors_with_untyped (Boolean)

Returns:

  • (Parlour::Types::Type)


262
263
264
265
266
267
268
# File 'lib/sord/type_converter.rb', line 262

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) ⇒ Array<String>

Given a string of YARD type parameters (without angle brackets), splits the string into an array of each type parameter.

Parameters:

  • params (String)

    The type parameters.

Returns:

  • (Array<String>)

    The split type parameters.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/sord/type_converter.rb', line 53

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, config) ⇒ Parlour::Types::Type

Converts a YARD type into a Parlour type.

Parameters:

  • yard (Boolean, Array, String)

    The YARD type.

  • item (YARD::CodeObjects::Base)

    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.

  • config (Configuration)

    The generation configuration.

Returns:

  • (Parlour::Types::Type)


126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/sord/type_converter.rb', line 126

def self.yard_to_parlour(yard, item, config)
  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, config) }
      .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 config.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
    if config.output_language == :rbs && (type = duck_type_to_rbs_type(yard))
      Logging.duck("#{yard} looks like a duck type with an equivalent RBS interface, replacing with #{type.generate_rbs}", item)
      type
    else
      Logging.duck("#{yard} looks like a duck type, replacing with untyped", item)
      Parlour::Types::Untyped.new
    end
  when /^#{GENERIC_TYPE_REGEX}$/
    generic_type = $1
    type_parameters = $2

    # If we don't do this, `const_defined?` will resolve "::Array" as the actual Ruby `Array`
    # type, not `Parlour::Types::Array`!
    relative_generic_type = generic_type.start_with?('::') \
      ? generic_type[2..-1] : generic_type

    parameters = split_type_parameters(type_parameters)
      .map { |x| yard_to_parlour(x, item, config) }
    if SINGLE_ARG_GENERIC_TYPES.include?(relative_generic_type) && parameters.length > 1
      Parlour::Types.const_get(relative_generic_type).new(Parlour::Types::Union.new(parameters))
    elsif relative_generic_type == 'Class' && parameters.length == 1
      Parlour::Types::Class.new(parameters.first)
    elsif relative_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, config.replace_errors_with_untyped)
      end
    else
      if Parlour::Types.constants.include?(relative_generic_type.to_sym)
        # This generic is built in to parlour, but sord doesn't
        # explicitly know about it.
        Parlour::Types.const_get(relative_generic_type).new(*parameters)
      else
        # This is a user defined generic
        Parlour::Types::Generic.new(
          yard_to_parlour(generic_type, nil, config),
          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, config) }
    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, config) }
    # 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, config.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, config) }
    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, config.replace_errors_with_untyped)
  end
end