class Sorbet::Private::HiddenMethodFinder
def self.main
def self.main self.new.main end
def self.output_file
def self.output_file PATH end
def all_modules_and_aliases
def all_modules_and_aliases puts "Naming all Modules" [constant_cache.all_module_names.sort, constant_cache.all_module_aliases] end
def capture_stderr
def capture_stderr real_stderr = $stderr $stderr = StringIO.new yield $stderr.string ensure $stderr = real_stderr end
def categorize(line)
def categorize(line) e.start_with?('#') rn :errors :hidden
def constant_cache
def constant_cache @cache ||= Sorbet::Private::ConstantLookupCache.new @cache end
def gen_source_rbi(classes, aliases)
def gen_source_rbi(classes, aliases) puts "Generating #{TMP_RBI} with #{classes.count} modules and #{aliases.count} aliases" serializer = Sorbet::Private::Serialize.new(constant_cache) buffer = [] buffer << Sorbet::Private::Serialize.header # should we do something with these errors? capture_stderr do classes.each do |class_name| buffer << serializer.class_or_module(class_name) end aliases.each do |base, other_names| other_names.each do |other_name| buffer << serializer.alias(base, other_name) end end end File.write(TMP_RBI, buffer.join("\n")) end
def looks_like_stub_name(name)
def looks_like_stub_name(name) name.include?('$') end
def main
def main mk_dir require_everything classes, aliases = all_modules_and_aliases gen_source_rbi(classes, aliases) rm_rbis write_constants source, rbi = read_constants write_diff(source, rbi) split_rbi rm_dir end
def mk_dir
def mk_dir FileUtils.mkdir_p(PATH) unless Dir.exist?(PATH) end
def read_constants
def read_constants puts "Reading #{SOURCE_CONSTANTS}" source = JSON.parse(File.read(SOURCE_CONSTANTS)) puts "Reading #{RBI_CONSTANTS}" rbi = JSON.parse(File.read(RBI_CONSTANTS)) [source, rbi] end
def real_name(mod)
def real_name(mod) constant_cache.name_by_class(mod) end
def require_everything
def require_everything puts "Requiring all of your code" Sorbet::Private::RequireEverything.require_everything end
def rm_dir
def rm_dir FileUtils.rm_r(TMP_PATH) end
def rm_rbis
def rm_rbis elete(HIDDEN_RBI) if File.exist?(HIDDEN_RBI) elete(ERRORS_RBI) if File.exist?(ERRORS_RBI)
def serialize_alias(source_entry, rbi_entry, my_klass, source_symbols, rbi_symbols)
def serialize_alias(source_entry, rbi_entry, my_klass, source_symbols, rbi_symbols) return if rbi_entry["kind"] != "STATIC_FIELD" return if source_entry == rbi_entry if source_entry is_stub = source_entry['superClass'] && source_symbols[source_entry['superClass']] == 'Sorbet::Private::Static::StubModule' if !is_stub return end end return if !rbi_entry["aliasTo"] fqn = rbi_symbols[rbi_entry["id"]] other_fqn = rbi_symbols[rbi_entry["aliasTo"]] return if looks_like_stub_name(fqn) ret = String.new ret << "#{fqn} = #{other_fqn}\n" return ret end
def serialize_class(source_entry, rbi_entry, klass, source_symbols, rbi_symbols, source_by_name)
def serialize_class(source_entry, rbi_entry, klass, source_symbols, rbi_symbols, source_by_name) return if rbi_entry["kind"] != "CLASS" name = rbi_entry["name"]["name"] if name.start_with?('<Class:') name = name.sub('<Class:', '').sub('>', '') my_klass_is_singleton = true else my_klass_is_singleton = false end begin my_klass = klass.const_get(name, false) # rubocop:disable PrisonGuard/NoDynamicConstAccess rescue LoadError, NameError, ArgumentError => e return "# #{e.message.gsub("\n", "\n# ")}" end return if !Sorbet::Private::RealStdlib.real_is_a?(my_klass, Class) && !Sorbet::Private::RealStdlib.real_is_a?(my_klass, Module) # We specifically don't typecheck anything in T:: since it is hardcoded # into sorbet return if real_name(my_klass) == 'T' source_type = nil if !source_entry if source_by_name[name] source_type = source_by_name[name]["kind"] end else source_type = source_entry["kind"] end if source_type && source_type != "CLASS" return "# The source says #{real_name(my_klass)} is a #{source_type} but reflection says it is a #{rbi_entry['kind']}" end if !source_entry source_children = [] source_mixins = [] is_stub = true else source_children = source_entry.fetch("children", []) source_mixins = source_entry.fetch("mixins", []) is_stub = source_entry['superClass'] && source_symbols[source_entry['superClass']] == 'Sorbet::Private::Static::StubModule' end rbi_children = rbi_entry.fetch("children", []) rbi_mixins = rbi_entry.fetch("mixins", []) methods = serialize_methods(source_children, rbi_children, my_klass, my_klass_is_singleton) includes = serialize_includes(source_mixins, rbi_mixins, my_klass, my_klass_is_singleton, source_symbols, rbi_symbols) values = serialize_values(source_children, rbi_children, my_klass, source_symbols) ret = [] if !without_errors(methods).empty? || !without_errors(includes).empty? || !without_errors(values).empty? || is_stub fqn = real_name(my_klass) if fqn klass_str = String.new klass_str << (Sorbet::Private::RealStdlib.real_is_a?(my_klass, Class) ? "class #{fqn}\n" : "module #{fqn}\n") klass_str << includes.join("\n") klass_str << "\n" unless klass_str.end_with?("\n") klass_str << methods.join("\n") klass_str << "\n" unless klass_str.end_with?("\n") klass_str << values.join("\n") klass_str << "\n" unless klass_str.end_with?("\n") klass_str << "end\n" ret << klass_str end end children = serialize_constants(source_children, rbi_children, my_klass, my_klass_is_singleton, source_symbols, rbi_symbols) if children != "" ret << children end ret.empty? ? nil : ret.join("\n") end
def serialize_constants(source, rbi, klass, is_singleton, source_symbols, rbi_symbols)
def serialize_constants(source, rbi, klass, is_singleton, source_symbols, rbi_symbols) source_by_name = source.map {|v| [v["name"]["name"], v]}.to_h ret = [] rbi.each do |rbi_entry| source_entry = source_by_name[rbi_entry["name"]["name"]] ret << serialize_alias(source_entry, rbi_entry, klass, source_symbols, rbi_symbols) ret << serialize_class(source_entry, rbi_entry, klass, source_symbols, rbi_symbols, source_by_name) end ret.compact.join("\n") end
def serialize_includes(source, rbi, klass, is_singleton, source_symbols, rbi_symbols)
def serialize_includes(source, rbi, klass, is_singleton, source_symbols, rbi_symbols) [] _mixins = source.map {|id| source_symbols[id]} xins = rbi.map {|id| rbi_symbols[id]} xins.each do |rbi_mixin| source_mixins.include?(rbi_mixin) yword = is_singleton ? "extend" : "include" t << " #{keyword} ::#{rbi_mixin}"
def serialize_methods(source, rbi, klass, is_singleton)
def serialize_methods(source, rbi, klass, is_singleton) _by_name = source.map {|v| [v["name"]["name"], v]}.to_h [] = Sorbet::Private::Serialize.new(constant_cache) ch do |rbi_entry| if rbi_entry["kind"] != "METHOD" = rbi_entry["name"]["name"] if source_by_name[name] if BLACKLIST.include?([klass.object_id, name]) if name.start_with?('<') && name.end_with?('>') n is_singleton method = klass.singleton_method(name) se method = klass.instance_method(name) d ue => e t << "# #{e.message.gsub("\n", "\n# ")}" xt r_method = method.super_method xt if super_method && T::AbstractUtils.abstract_method?(method) == T::AbstractUtils.abstract_method?(super_method) rs = capture_stderr do t << maker.serialize_method(method, is_singleton, with_sig: false) rs.split("\n").each do |line| t << "# #{line}"
def serialize_values(source, rbi, klass, source_symbols)
def serialize_values(source, rbi, klass, source_symbols) _by_name = source.map {|v| [v["name"]["name"], v]}.to_h [] ch do |rbi_entry| = rbi_entry["name"]["name"] ce_entry = source_by_name[name] ource_entry _stub = source_entry['superClass'] && source_symbols[source_entry['superClass']] == 'Sorbet::Private::Static::StubModule' xt unless is_stub if Sorbet::Private::ConstantLookupCache::DEPRECATED_CONSTANTS.include?("#{Sorbet::Private::RealStdlib.real_name(klass)}::#{name}") n _value = klass.const_get(name, false) # rubocop:disable PrisonGuard/NoDynamicConstAccess ue StandardError, LoadError => e t << "# #{e.message.gsub("\n", "\n# ")}" xt if Sorbet::Private::RealStdlib.real_is_a?(my_value, Class) || Sorbet::Private::RealStdlib.real_is_a?(my_value, Module) << " #{name} = ::T.let(nil, ::T.untyped)"
def split_rbi
def split_rbi Generating split RBIs into #{PATH}" = { en: String.new, rs: String.new, = File.read(DIFF_RBI) tput = T.let(nil, T.untyped) split("\n").each do |line| gory = categorize(line) ategory == :errors Don't ever switch to errors output permanantly tput[category] << line + "\n" xt category.nil? r_output = output[category] output << line + "\n" rite(HIDDEN_RBI, HEADER + "\n" + output[:hidden]) rite(ERRORS_RBI, HEADER + "\n" + output[:errors])
def symbols_id_to_name(entry, prefix)
def symbols_id_to_name(entry, prefix) ret = {} symbols_id_to_name_real(entry, prefix, ret) ret end
def symbols_id_to_name_real(entry, prefix, ret)
def symbols_id_to_name_real(entry, prefix, ret) entry["name"]["name"] fix == '' || prefix == "<root>" = name.to_s = "#{prefix}::#{name}" try["id"]] = fqn fetch("children", []).each do |child| ols_id_to_name_real(child, fqn, ret)
def without_errors(lines)
def without_errors(lines) reject {|line| line.start_with?("#")}
def write_constants
def write_constants puts "Printing your code's symbol table into #{SOURCE_CONSTANTS}" io = IO.popen( [ File.realpath("#{__dir__}/../bin/srb"), 'tc', '--print=symbol-table-full-json', '--stdout-hup-hack', '--silence-dev-message', '--no-error-count', ], err: SOURCE_CONSTANTS_ERR ) File.write(SOURCE_CONSTANTS, io.read) io.close raise "Your source can't be read by Sorbet.\nYou can try `find . -type f | xargs -L 1 -t bundle exec srb tc --no-config --error-white-list 1000` and hopefully the last file it is processing before it dies is the culprit.\nIf not, maybe the errors in this file will help: #{SOURCE_CONSTANTS_ERR}" if File.read(SOURCE_CONSTANTS).empty? puts "Printing #{TMP_RBI}'s symbol table into #{RBI_CONSTANTS}" io = IO.popen( [ File.realpath("#{__dir__}/../bin/srb"), 'tc', # Make sure we don't load a sorbet/config in your cwd '--no-config', '--print=symbol-table-full-json', # Method redefined with mismatched argument is ok since sometime # people monkeypatch over method '--error-black-list=4010', # Redefining constant is needed because we serialize things both as # aliases and in-class constants. '--error-black-list=4012', # Invalid nesting is ok because we don't generate all the intermediate # namespaces for aliases '--error-black-list=4015', '--stdout-hup-hack', '--silence-dev-message', '--no-error-count', TMP_RBI, ], err: RBI_CONSTANTS_ERR ) File.write(RBI_CONSTANTS, io.read) io.close raise "#{TMP_RBI} had unexpected errors. Check this file for a clue: #{RBI_CONSTANTS_ERR}" unless $?.success? end
def write_diff(source, rbi)
def write_diff(source, rbi) puts "Building rbi id to symbol map" rbi_symbols = symbols_id_to_name(rbi, '') puts "Building source id to symbol map" source_symbols = symbols_id_to_name(source, '') puts "Writing #{DIFF_RBI}" diff = serialize_constants( source.fetch("children", []), rbi.fetch("children", []), Object, false, source_symbols, rbi_symbols) File.write(DIFF_RBI, diff) end