module SorbetCFG::Loader

Public Class Methods

foo(a, b) click to toggle source
# File lib/sorbet-cfg/loader.rb, line 17
def self.foo(a, b)
  a + b
end
generate_cfg_export_rbi(target) click to toggle source

Given a module or class, returns the contents of an RBI file which, when loaded, makes that module or class extend T::CFGExport so that Sorbet's “-p cfg-json” option will print JSON for its CFG.

# File lib/sorbet-cfg/loader.rb, line 55
def self.generate_cfg_export_rbi(target)
  result_lines = []

  # Start from the top
  current_object = T.let(Kernel, T.untyped)

  total_nesting = 0
  target.to_s.split('::').each.with_index do |part_name, i|
    current_object = current_object.const_get(part_name)

    if current_object.is_a?(Class)
      kind = "class"
    elsif current_object.is_a?(Module)
      kind = "module"
    else
      raise "#{current_object} is neither a class nor a module"
    end

    result_lines << "#{'  ' * i}#{kind} #{part_name}"
    total_nesting = i
  end

  result_lines << "#{'  ' * (total_nesting + 1)}extend T::CFGExport"

  (total_nesting + 1).times do |i|
    this_level = total_nesting - i
    result_lines << "#{'  ' * this_level}end"
  end

  result_lines.join("\n") + "\n"
end
index() click to toggle source
# File lib/sorbet-cfg/loader.rb, line 12
def self.index
  @index ||= {}
end
index_module(obj) click to toggle source
# File lib/sorbet-cfg/loader.rb, line 88
def self.index_module(obj)
  rbi_contents = generate_cfg_export_rbi(obj)
  rbi_path = '/tmp/sorbet-cfg-export.rbi'
  File.write(rbi_path, rbi_contents)

  result = T.let(nil, T.nilable(Tree::MultiCFG))

  all_methods = obj.methods + obj.instance_methods
  raise "can\'t get the source location of #{obj} because it contains no methods" if all_methods.empty?

  # Just pick the first one, it's easiest
  # TODO: might be best to pick all unique, though?
  target = obj.method(T.must(all_methods[0]))
  project = project_root(Utilities.true_source_location(target)[0])
  
  raise 'unable to locate project root' unless project

  Open3.popen3('srb', 'tc', rbi_path, '-p', 'cfg-json', chdir: project) do |i, o, e, t|
    json_docs = o.read

    # TODO: VERY BAD
    # Just all of this is terrible
    # There's a stub element at the end because trailing commas aren't valid JSON
    cursed_json_doc = "[#{json_docs.gsub(/^\}/, '},')} null]"
    parsed_json = JSON.parse(cursed_json_doc)[0...-1]
    result = Tree::MultiCFG.new(cfg: parsed_json.map { |x| Tree::CFG.from_hash(x) })
  end

  raise "indexing failed for #{obj}" unless result
  index[obj] = {}
  result.cfg.each do |cfg|
    if cfg.definition_full_name.include?('.')
      prefix_char = '.'
    elsif cfg.definition_full_name.include?('#')
      prefix_char = '#'
    else
      raise "unable to determine type of #{cfg}"
    end

    def_name = "#{prefix_char}#{cfg.definition_full_name.split(prefix_char).last}"
    T.must(index[obj])[def_name] = cfg
  end
end
project_root(path) click to toggle source

Given an ABSOLUTE path to somewhere within a Sorbet-enabled Ruby project, finds the root of the project by looking for a Gemfile or .bundle, as well as a sorbet directory.

# File lib/sorbet-cfg/loader.rb, line 26
def self.project_root(path)
  # TODO: Sorbet is currently Unix-only, so this code is too

  raise 'path does not appear to be absolute' unless path.start_with?('/')

  possible_paths = []
  Pathname.new(path).each_filename do |part|
    possible_paths << (possible_paths.empty? \
      ? "/#{part}"
      : File.join(possible_paths.last, part))
  end

  possible_paths.reverse.each do |possible_path|
    has_bundler = File.exist?(File.join(possible_path, 'Gemfile')) ||
      File.exist?(File.join(possible_path, '.bundle'))

    has_sorbet = File.exist?(File.join(possible_path, 'sorbet'))

    return possible_path if has_bundler && has_sorbet
  end

  nil
end