module Steep module Drivers class Scaffold attr_reader :source_paths attr_reader :stdout attr_reader :stderr attr_reader :labeling include Utils::EachSignature def initialize(source_paths:, stdout:, stderr:) @source_paths = source_paths @stdout = stdout @stderr = stderr @labeling = ASTUtils::Labeling.new end def run each_ruby_source(source_paths, false) do |source| Generator.new(source: source, stderr: stderr).write(io: stdout) end end class Generator class Module attr_reader :name attr_reader :methods attr_reader :singleton_methods attr_reader :ivars attr_reader :kind def initialize(name:, kind:) @kind = kind @name = name @ivars = {} @methods = {} @singleton_methods = {} end def class? kind == :class end def module? kind == :module end attr_accessor :has_subclass def namespace_class? has_subclass && methods.empty? && singleton_methods.empty? end end attr_reader :source attr_reader :modules attr_reader :constants attr_reader :stderr def initialize(source:, stderr:) @source = source @stderr = stderr @modules = [] @constants = {} end def write(io:) generate(source.node, current_path: []) modules.each do |mod| unless mod.namespace_class? io.puts "#{mod.kind} #{mod.name}" mod.ivars.each do |name, type| io.puts " #{name}: #{type}" end mod.methods.each do |name, type| io.puts " def #{name}: #{type}" end mod.singleton_methods.each do |name, type| io.puts " def self.#{name}: #{type}" end io.puts "end" io.puts end end constants.each do |name, ty| io.puts "#{name}: #{ty}" end end def module_name(name) if name.type == :const prefix = name.children[0] if prefix "#{module_name(prefix)}::#{name.children[1]}" else name.children[1].to_s end else stderr.puts "Unexpected node for class name: #{name}" return "____" end end def full_name(current_path, name) (current_path + [name]).join("::") end def generate(node, current_path:, current_module: nil, is_instance_method: false) case node.type when :module name = module_name(node.children[0]) mod = Module.new(name: full_name(current_path, name), kind: :module) modules << mod if node.children[1] generate(node.children[1], current_path: current_path + [name], current_module: mod) end if current_module current_module.has_subclass = true end when :class name = module_name(node.children[0]) klass = Module.new(name: full_name(current_path, name), kind: :class) modules << klass if node.children[2] generate(node.children[2], current_path: current_path + [name], current_module: klass) end if current_module current_module.has_subclass = true end when :def name, args, body = node.children if current_module current_module.methods[name] = "(#{arg_types(args)}) -> #{guess_type(body)}" end if body generate(body, current_path: current_path, current_module: current_module, is_instance_method: true) end when :ivar, :ivasgn name = node.children[0] if current_module && is_instance_method current_module.ivars[name] = guess_type(node.children[1]) end each_child_node(node) do |child| generate(child, current_path: current_path, current_module: current_module, is_instance_method: is_instance_method) end when :defs if node.children[0].type == :self _, name, args, body = node.children if current_module current_module.singleton_methods[name] = "(#{arg_types(args)}) -> #{guess_type(body)}" end if body generate(body, current_path: current_path, current_module: current_module, is_instance_method: false) end end when :casgn if node.children[0] stderr.puts "Unexpected casgn: #{node}, #{node.loc.line}" end constants[full_name(current_path, node.children[1])] = guess_type(node.children[2]) if node.children[2] generate(node.children[2], current_path: current_path, current_module: current_module, is_instance_method: is_instance_method) end else each_child_node(node) do |child| generate(child, current_path: current_path, current_module: current_module, is_instance_method: is_instance_method) end end end def guess_type(node) return "any" unless node case node.type when :false, :true "_Boolean" when :int "Integer" when :float "Float" when :complex "Complex" when :rational "Rational" when :str, :dstr, :xstr "String" when :sym, :dsym "Symbol" when :regexp "Regexp" when :array "Array" when :hash "Hash" when :irange, :erange "Range" when :lvasgn, :ivasgn, :cvasgn, :gvasgn, :casgn guess_type(node.children.last) when :send if node.children[1] == :[]= guess_type(node.children.last) else "any" end when :begin # should support shortcut return? guess_type(node.children.last) when :return children = node.children if children.size == 1 guess_type(node.children.last) else "Array" # or Tuple or any? end when :if children = node.children if children[2] ty1 = guess_type(children[1]) ty2 = guess_type(children[2]) if ty1 == ty2 ty1 else "any" end else "void" # assuming no-else if statement implies void end when :while, :until, :while_post, :until_post, :for "void" when :case children = node.children if children.last ty = guess_type(children.last) children[1..-2].each do |child| return "any" if ty != guess_type(child.children.last) end ty else "any" end when :masgn "void" # assuming masgn implies void else "any" end end def each_child_node(node, &block) node.children.each do |child| if child.is_a?(::AST::Node) yield child end end end def arg_types(args) args.children.map do |arg| case arg.type when :arg "any" when :optarg "?#{guess_type(arg.children[1])}" when :restarg "*any" when :kwarg "#{arg.children.first.name}: any" when :kwoptarg "?#{arg.children.first.name}: #{guess_type(arg.children[1])}" when :kwrestarg "**any" end end.compact.join(", ") end end end end end