Class: Sord::RbiGenerator

Inherits:
Object
  • Object
show all
Defined in:
lib/sord/rbi_generator.rb

Overview

Converts the current working directory’s YARD registry into an RBI file.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeRbiGenerator

Create a new RBI generator.



19
20
21
22
23
24
25
26
27
# File 'lib/sord/rbi_generator.rb', line 19

def initialize
  @rbi_contents = ['# typed: strong']
  @object_count = 0

  # Hook the logger so that messages are added as comments to the RBI file
  Logging.add_hook do |type, msg, item|
    rbi_contents << "  # sord #{type} - #{msg}"
  end
end

Instance Attribute Details

#object_countInteger (readonly)

Returns The number of objects this generator has processed so far.

Returns:

  • (Integer)

    The number of objects this generator has processed so far.



15
16
17
# File 'lib/sord/rbi_generator.rb', line 15

def object_count
  @object_count
end

#rbi_contentsArray<String> (readonly)

Returns The lines of the generated RBI file so far.

Returns:

  • (Array<String>)

    The lines of the generated RBI file so far.



11
12
13
# File 'lib/sord/rbi_generator.rb', line 11

def rbi_contents
  @rbi_contents
end

Instance Method Details

#add_methods(item) ⇒ void

This method returns an undefined value.

Given a YARD NamespaceObject, add lines defining its methods and their signatures to the current RBI file.

Parameters:

  • item (YARD::CodeObjects::NamespaceObject)


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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/sord/rbi_generator.rb', line 55

def add_methods(item)
  # TODO: block documentation

  item.meths.each do |meth|
    count_object

    parameter_list = meth.parameters.map do |name, default|
      # Handle these three main cases:
      # - def method(param) or def method(param:)
      # - def method(param: 'default')
      # - def method(param = 'default')
      if default.nil?
        "#{name}"
      elsif !default.nil? && name.end_with?(':')
        "#{name} #{default}"
      else
        "#{name} = #{default}"
      end
    end.join(", ")

    # This is better than iterating over YARD's "@param" tags directly 
    # because it includes parameters without documentation
    parameter_names_to_tags = meth.parameters.map do |name, _|
      [name, meth.tags('param').find { |p| p.name == name }]
    end.to_h

    sig_params_list = parameter_names_to_tags.map do |name, tag|
      if tag
        "#{name}: #{TypeConverter.yard_to_sorbet(tag.types, meth)}"
      elsif name.start_with? '*'
        # TODO: is there a YARD definition for this?
        "args: T::Array[T.any]"
      elsif meth.path.end_with? '='
        # Look for the matching getter method
        getter_path = meth.path[0...-1]
        getter = item.meths.find { |m| m.path == getter_path }

        unless getter
          Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth)
          next "#{name}: T.untyped"
        end

        inferred_type = TypeConverter.yard_to_sorbet(
          getter.tags('return').flat_map(&:types), meth)
        
        Logging.infer("inferred type of parameter #{name.inspect} as #{inferred_type} using getter's return type", meth)
        # Get rid of : on keyword arguments.
        name = name.chop if name.end_with?(':')
        "#{name}: #{inferred_type}"
      else
        Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth)
        # Get rid of : on keyword arguments.
        name = name.chop if name.end_with?(':')
        "#{name}: T.untyped"
      end
    end.join(", ")

    return_tags = meth.tags('return')
    returns = if return_tags.length == 0
      "void"
    elsif return_tags.length == 1 && return_tags.first.types.first.downcase == "void"
      "void"
    else
      "returns(#{TypeConverter.yard_to_sorbet(meth.tag('return').types, meth)})"
    end

    prefix = meth.scope == :class ? 'self.' : ''

    sig = sig_params_list.empty? ? "  sig { #{returns} }" : "  sig { params(#{sig_params_list}).#{returns} }"
    rbi_contents << sig

    rbi_contents << "  def #{prefix}#{meth.name}(#{parameter_list}); end"
  end
end

#add_mixins(item) ⇒ void

This method returns an undefined value.

Given a YARD CodeObject, add lines defining its mixins (that is, extends and includes) to the current RBI file.

Parameters:

  • item (YARD::CodeObjects::Base)


39
40
41
42
43
44
45
46
47
48
49
# File 'lib/sord/rbi_generator.rb', line 39

def add_mixins(item)
  extends = item.instance_mixins
  includes = item.class_mixins

  extends.each do |this_extend|
    rbi_contents << "  extend #{this_extend.path}"
  end
  includes.each do |this_include|
    rbi_contents << "  include #{this_include.path}"
  end
end

#count_objectvoid

This method returns an undefined value.

Increment the object counter.



31
32
33
# File 'lib/sord/rbi_generator.rb', line 31

def count_object
  @object_count += 1
end

#run(filename) ⇒ void

This method returns an undefined value.

Generates the RBI file and writes it to the given file path.

Parameters:

  • filename (String)


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
# File 'lib/sord/rbi_generator.rb', line 133

def run(filename)
  # Get YARD ready
  YARD::Registry.load!

  # TODO: constants?

  # Populate the RBI with modules first
  YARD::Registry.all(:module).each do |item|
    count_object
    
    rbi_contents << "module #{item.path}"
    add_mixins(item)
    add_methods(item)
    rbi_contents << "end"
  end

  # Now populate with classes
  YARD::Registry.all(:class).each do |item|
    count_object

    superclass = (item.superclass if item.superclass.to_s != "Object")
    rbi_contents << "class #{item.path} #{"< #{superclass}" if superclass}" 
    add_mixins(item)
    add_methods(item)        
    rbi_contents << "end"
  end

  # Write the file
  raise "no filename specified" unless filename
  File.write(filename, rbi_contents.join(?\n))

  Logging.done("Processed #{object_count} objects")
rescue
  Logging.error($!)
  $@.each do |line|
    puts "         #{line}".light_white
  end
end