module TypeProf module Reporters module_function def generate_analysis_trace(state, visited, backward_edge) return nil if visited[state] visited[state] = true prev_states = backward_edges[state] if prev_states prev_states.each_key do |pstate| trace = generate_analysis_trace(pstate, visited, backward_edge) return [state] + trace if trace end nil else [] end end def filter_backtrace(trace) ntrace = [trace.first] trace.each_cons(2) do |ep1, ep2| ntrace << ep2 if ep1.ctx != ep2.ctx end ntrace end def show_message(terminated, output) if Config.options[:show_typeprof_version] output.puts "# TypeProf #{ VERSION }" output.puts end if terminated output.puts "# CAUTION: Type profiling was terminated prematurely because of the limitation" output.puts end end def show_error(errors, backward_edge, output) return if errors.empty? return unless Config.options[:show_errors] output.puts "# Errors" errors.each do |ep, msg| if ENV["TP_DETAIL"] backtrace = filter_backtrace(generate_analysis_trace(ep, {}, backward_edge)) else backtrace = [ep] end loc, *backtrace = backtrace.map do |ep| ep&.source_location end output.puts "#{ loc }: #{ msg }" backtrace.each do |loc| output.puts " from #{ loc }" end end output.puts end def show_reveal_types(scratch, reveal_types, output) return if reveal_types.empty? output.puts "# Revealed types" reveal_types.each do |source_location, ty| output.puts "# #{ source_location } #=> #{ ty.screen_name(scratch) }" end output.puts end def show_gvars(scratch, gvars, output) # A signature for global variables is not supported in RBS return if gvars.dump.empty? output.puts "# Global variables" gvars.dump.each do |gvar_name, entry| next if entry.type == Type.bot s = entry.rbs_declared ? "#" : "" output.puts s + "#{ gvar_name }: #{ entry.type.screen_name(scratch) }" end output.puts end end class RubySignatureExporter def initialize( scratch, class_defs, iseq_method_to_ctxs ) @scratch = scratch @class_defs = class_defs @iseq_method_to_ctxs = iseq_method_to_ctxs end def conv_class(namespace, class_def, inner_classes) @scratch.namespace = namespace if class_def.klass_obj.superclass != :__root__ && class_def.klass_obj.superclass omit = class_def.klass_obj.superclass == Type::Builtin[:obj] || class_def.klass_obj == Type::Builtin[:obj] superclass = omit ? nil : @scratch.get_class_name(class_def.klass_obj.superclass) type_args = class_def.klass_obj.superclass_type_args if type_args && !type_args.empty? superclass += "[#{ type_args.map {|ty| ty.screen_name(@scratch) }.join(", ") }]" end end @scratch.namespace = class_def.name consts = {} class_def.consts.each do |name, (ty, absolute_path)| next if ty.is_a?(Type::Class) next if !absolute_path || Config.check_dir_filter(absolute_path) == :exclude consts[name] = ty.screen_name(@scratch) end modules = class_def.modules.to_h do |kind, mods| mods = mods.to_h do |singleton, mods| mods = mods.filter_map do |mod_def, _type_args, absolute_paths| next if absolute_paths.all? {|path| !path || Config.check_dir_filter(path) == :exclude } Type::Instance.new(mod_def.klass_obj).screen_name(@scratch) end [singleton, mods] end [kind, mods] end visibilities = {} source_locations = {} methods = {} ivars = class_def.ivars.dump cvars = class_def.cvars.dump class_def.methods.each do |(singleton, mid), mdefs| mdefs.each do |mdef| case mdef when ISeqMethodDef ctxs = @iseq_method_to_ctxs[mdef] next unless ctxs ctxs.each do |ctx| next if mid != ctx.mid next if Config.check_dir_filter(ctx.iseq.absolute_path) == :exclude method_name = ctx.mid method_name = "self.#{ method_name }" if singleton key = [:iseq, method_name] visibilities[key] ||= mdef.pub_meth source_locations[key] ||= ctx.iseq.source_location(0) (methods[key] ||= []) << @scratch.show_method_signature(ctx) end when AliasMethodDef alias_name, orig_name = mid, mdef.orig_mid if singleton alias_name = "self.#{ alias_name }" orig_name = "self.#{ orig_name }" end key = [:alias, alias_name] visibilities[key] ||= mdef.pub_meth source_locations[key] ||= mdef.def_ep&.source_location methods[key] = orig_name when AttrMethodDef next if !mdef.def_ep absolute_path = mdef.def_ep.ctx.iseq.absolute_path next if !absolute_path || Config.check_dir_filter(absolute_path) == :exclude mid = mid.to_s[0..-2].to_sym if mid.to_s.end_with?("=") method_name = mid method_name = "self.#{ mid }" if singleton method_name = [method_name, :"@#{ mid }" != mdef.ivar] key = [:attr, method_name] visibilities[key] ||= mdef.pub_meth source_locations[key] ||= mdef.def_ep.source_location if methods[key] if methods[key][0] != mdef.kind methods[key][0] = :accessor end else entry = ivars[[singleton, mdef.ivar]] ty = entry ? entry.type : Type.any methods[key] = [mdef.kind, ty.screen_name(@scratch), ty.include_untyped?(@scratch)] end when TypedMethodDef if mdef.rbs_source method_name, sigs = mdef.rbs_source key = [:rbs, method_name] methods[key] = sigs visibilities[key] ||= mdef.pub_meth end end end end ivars = ivars.map do |(singleton, var), entry| next if entry.absolute_paths.all? {|path| Config.check_dir_filter(path) == :exclude } ty = entry.type next unless var.to_s.start_with?("@") var = "self.#{ var }" if singleton next if methods[[:attr, [singleton ? "self.#{ var.to_s[1..] }" : var.to_s[1..].to_sym, false]]] next if entry.rbs_declared [var, ty.screen_name(@scratch)] end.compact cvars = cvars.map do |var, entry| next if entry.absolute_paths.all? {|path| Config.check_dir_filter(path) == :exclude } next if entry.rbs_declared [var, entry.type.screen_name(@scratch)] end.compact if !class_def.absolute_path || Config.check_dir_filter(class_def.absolute_path) == :exclude if methods.keys.all? {|type,| type == :rbs } return nil if consts.empty? && modules[:before][true].empty? && modules[:before][false].empty? && modules[:after][true].empty? && modules[:after][false].empty? && ivars.empty? && cvars.empty? && inner_classes.empty? end end @scratch.namespace = nil ClassData.new( kind: class_def.kind, name: class_def.name, superclass: superclass, consts: consts, modules: modules, ivars: ivars, cvars: cvars, methods: methods, visibilities: visibilities, source_locations: source_locations, inner_classes: inner_classes, ) end ClassData = Struct.new(:kind, :name, :superclass, :consts, :modules, :ivars, :cvars, :methods, :visibilities, :source_locations, :inner_classes, keyword_init: true) def show(stat_eps, output) # make the class hierarchy root = {} @class_defs.each_value do |class_def| h = root class_def.name.each do |name| h = h[name] ||= {} end h[:class_def] = class_def end hierarchy = build_class_hierarchy([], root) output.puts "# Classes" # and Modules prev_nil = true show_class_hierarchy(0, hierarchy).each do |line| if line == nil output.puts line unless prev_nil prev_nil = true else output.puts line prev_nil = false end end if ENV["TP_STAT"] output.puts "" output.puts "# TypeProf statistics:" output.puts "# %d execution points" % stat_eps.size end if ENV["TP_COVERAGE"] coverage = {} stat_eps.each do |ep| path = ep.ctx.iseq.path lineno = ep.ctx.iseq.linenos[ep.pc] - 1 (coverage[path] ||= [])[lineno] ||= 0 (coverage[path] ||= [])[lineno] += 1 end File.binwrite("typeprof-analysis-coverage.dump", Marshal.dump(coverage)) end end def build_class_hierarchy(namespace, hierarchy) hierarchy.map do |name, h| class_def = h.delete(:class_def) class_data = conv_class(namespace, class_def, build_class_hierarchy(namespace + [name], h)) class_data end.compact end def show_class_hierarchy(depth, hierarchy) lines = [] hierarchy.each do |class_data| lines << nil lines.concat show_class_data(depth, class_data) end lines end def show_const(namespace, path) return path.last.to_s if namespace == path i = 0 i += 1 while namespace[i] && namespace[i] == path[i] path[i..].join("::") end def show_class_data(depth, class_data) indent = " " * depth name = class_data.name.last superclass = " < " + class_data.superclass if class_data.superclass first_line = indent + "#{ class_data.kind } #{ name }#{ superclass }" lines = [] class_data.consts.each do |name, ty| lines << (indent + " #{ name }: #{ ty }") end class_data.modules.each do |kind, mods| mods.each do |singleton, mods| case when kind == :before && singleton then directive = nil when kind == :before && !singleton then directive = "prepend" when kind == :after && singleton then directive = "extend" when kind == :after && !singleton then directive = "include" end mods.each do |mod| lines << (indent + " #{ directive } #{ mod }") if directive end end end class_data.ivars.each do |var, ty| lines << (indent + " #{ var }: #{ ty }") unless var.start_with?("_") end class_data.cvars.each do |var, ty| lines << (indent + " #{ var }: #{ ty }") end lines << nil prev_vis = true class_data.methods.each do |key, arg| vis = class_data.visibilities[key] if prev_vis != vis lines << nil lines << (indent + " #{ vis ? "public" : "private" }") prev_vis = vis end source_location = class_data.source_locations[key] if Config.options[:show_source_locations] && source_location lines << nil lines << (indent + " # #{ source_location }") end type, (method_name, hidden) = key case type when :attr kind, ty, untyped = *arg exclude = Config.options[:exclude_untyped] && untyped ? "#" : " " # XXX lines << (indent + "#{ exclude } attr_#{ kind } #{ method_name }#{ hidden ? "()" : "" }: #{ ty }") when :rbs sigs = arg.sort.join("\n" + indent + "#" + " " * (method_name.size + 5) + "| ") lines << (indent + "# def #{ method_name }: #{ sigs }") when :iseq sigs = [] untyped = false arg.each do |sig, untyped0| sigs << sig untyped ||= untyped0 end sigs = sigs.sort.join("\n" + indent + " " * (method_name.size + 6) + "| ") exclude = Config.options[:exclude_untyped] && untyped ? "#" : " " # XXX lines << (indent + "#{ exclude } def #{ method_name }: #{ sigs }") when :alias orig_name = arg lines << (indent + " alias #{ method_name } #{ orig_name }") end end lines.concat show_class_hierarchy(depth + 1, class_data.inner_classes) lines.shift until lines.empty? || lines.first lines.pop until lines.empty? || lines.last lines.unshift first_line lines << (indent + "end") end end end