require 'parser/current' require 'rubrowser/parser/definition/class' require 'rubrowser/parser/definition/module' require 'rubrowser/parser/relation/base' require 'rubrowser/parser/file/builder' module Rubrowser module Parser class File FILE_SIZE_LIMIT = 2 * 1024 * 1024 attr_reader :file, :definitions, :relations def initialize(file) @file = ::File.absolute_path(file) @definitions = [] @relations = [] end def parse return unless valid_file?(file) constants = constants_from_file @definitions = constants[:definitions] @relations = constants[:relations] rescue ::Parser::SyntaxError warn "SyntaxError in #{file}" end def constants_from_file contents = ::File.read(file) buffer = ::Parser::Source::Buffer.new(file, 1) buffer.source = contents.force_encoding(Encoding::UTF_8) ast = parser.parse(buffer) parse_block(ast) end def parser parser = ::Parser::CurrentRuby.new(Builder.new) parser.diagnostics.ignore_warnings = true parser.diagnostics.all_errors_are_fatal = false parser end def valid_file?(file) !::File.symlink?(file) && ::File.file?(file) && ::File.size(file) <= FILE_SIZE_LIMIT end private def parse_block(node, parents = []) return empty_result unless valid_node?(node) case node.type when :module then parse_module(node, parents) when :class then parse_class(node, parents) when :const then parse_const(node, parents) else parse_array(node.children, parents) end end def parse_module(node, parents = []) namespace = ast_consts_to_array(node.children.first, parents) definition = build_definition(Definition::Module, namespace, node) constants = { definitions: [definition] } children_constants = parse_array(node.children[1..-1], namespace) merge_constants(children_constants, constants) end def build_definition(klass, namespace, node) klass.new( namespace, file: file, line: node.loc.line, lines: node.loc.last_line - node.loc.line + 1 ) end def parse_class(node, parents = []) namespace = ast_consts_to_array(node.children.first, parents) definition = build_definition(Definition::Class, namespace, node) constants = { definitions: [definition] } children_constants = parse_array(node.children[1..-1], namespace) merge_constants(children_constants, constants) end def parse_const(node, parents = []) constant = ast_consts_to_array(node) definition = Relation::Base.new( constant, parents, file: file, line: node.loc.line ) { relations: [definition] } end def parse_array(arr, parents = []) arr.map { |n| parse_block(n, parents) } .reduce { |a, e| merge_constants(a, e) } end def merge_constants(c1, c2) c1 ||= {} c2 ||= {} { definitions: c1[:definitions].to_a + c2[:definitions].to_a, relations: c1[:relations].to_a + c2[:relations].to_a } end def ast_consts_to_array(node, parents = []) return parents unless valid_node?(node) && %I[const cbase].include?(node.type) ast_consts_to_array(node.children.first, parents) + [node.children.last] end def empty_result {} end def valid_node?(node) node.is_a?(::Parser::AST::Node) end end end end