bf4/metric_fu

View on GitHub
lib/metric_fu/metrics/saikuro/saikuro.rb

Summary

Maintainability
B
4 hrs
Test Coverage
module MetricFu

  class Saikuro < Generator
    include Rake::DSL if defined?(Rake::DSL) # rake 0.8.7 and 0.9.2 compatible

    def emit
      options_string = MetricFu.saikuro.inject("") do |options, option|
        option[0] == :input_directory ? options : options + "--#{option.join(' ')} "
      end

      MetricFu.saikuro[:input_directory].each do |input_dir|
        options_string += "--input_directory #{input_dir} "
      end

      saikuro_bin= $:.map{|d| d+'/../bin/saikuro'}.select{|f| File.exists? f}.first || 'saikuro'
      sh %{#{saikuro_bin} #{options_string}} do |ok, response|
        unless ok
          puts "Saikuro failed with exit status: #{response.exitstatus}"
        end
      end
    end

    def format_directories
      dirs = MetricFu.saikuro[:input_directory].join(" | ")
      "\"#{dirs}\""
    end

    def analyze
      @files = sort_files(assemble_files)
      @classes = sort_classes(assemble_classes(@files))
      @meths = sort_methods(assemble_methods(@files))
    end

    def to_h
      files = @files.map do |file|
        my_file = file.to_h

        f = file.filepath
        f.gsub!(%r{^#{metric_directory}/}, '')
        f << "/#{file.filename}"

        my_file[:filename] = f
        my_file
      end
      @saikuro_data = {:files => files,
                       :classes => @classes.map {|c| c.to_h},
                       :methods => @meths.map {|m| m.to_h}
                      }
      {:saikuro => @saikuro_data}
    end

    def per_file_info(out)
      @saikuro_data[:files].each do |file_data|
        next if File.extname(file_data[:filename]) == '.erb' || !File.exists?(file_data[:filename])
        begin
          line_numbers = MetricFu::LineNumbers.new(File.open(file_data[:filename], 'r').read)
        rescue StandardError => e
          raise e unless e.message =~ /you shouldn't be able to get here/
          puts "ruby_parser blew up while trying to parse #{file_path}. You won't have method level Saikuro information for this file."
          next
        end

        out[file_data[:filename]] ||= {}
        file_data[:classes].each do |class_data|
          class_data[:methods].each do |method_data|
            line = line_numbers.start_line_for_method(method_data[:name])
            out[file_data[:filename]][line.to_s] ||= []
            out[file_data[:filename]][line.to_s] << {:type => :saikuro,
                                                      :description => "Complexity #{method_data[:complexity]}"}
          end
        end
      end
    end

    private
    def sort_methods(methods)
      methods.sort_by {|method| method.complexity.to_i}.reverse
    end

    def assemble_methods(files)
      methods = []
      files.each do |file|
        file.elements.each do |element|
          element.defs.each do |defn|
            defn.name = "#{element.name}##{defn.name}"
            methods << defn
          end
        end
      end
      methods
    end

    def sort_classes(classes)
      classes.sort_by {|k| k.complexity.to_i}.reverse
    end

    def assemble_classes(files)
      files.map {|f| f.elements}.flatten
    end

    def sort_files(files)
      files.sort_by do |file|
        file.elements.
             max {|a,b| a.complexity.to_i <=> b.complexity.to_i}.
             complexity.to_i
      end.reverse
    end

    def assemble_files
      files = []
      Dir.glob("#{metric_directory}/**/*.html").each do |path|
        if Saikuro::SFile.is_valid_text_file?(path)
          file = Saikuro::SFile.new(path)
          if file
            files << file
          end
        end
      end
      files
    end

  end

  class Saikuro::SFile

    attr_reader :elements

    def initialize(path)
      @path = path
      @file_handle = File.open(@path, "r")
      @elements = []
      get_elements
    ensure
      @file_handle.close if @file_handle
    end

    def self.is_valid_text_file?(path)
      File.open(path, "r") do |f|
        if f.eof? || !f.readline.match(/--/)
          return false
        else
          return true
        end
      end
    end

    def filename
      File.basename(@path, '_cyclo.html')
    end

    def filepath
      File.dirname(@path)
    end

    def to_h
      merge_classes
      {:classes => @elements}
    end

    def get_elements
      begin
        while (line = @file_handle.readline) do
          return [] if line.nil? || line !~ /\S/
          element ||= nil
          if line.match /START/
            unless element.nil?
              @elements << element
              element = nil
            end
            line = @file_handle.readline
            element = Saikuro::ParsingElement.new(line)
          elsif line.match /END/
            @elements << element if element
            element = nil
          else
            element << line if element
          end
        end
      rescue EOFError
        nil
      end
    end


    def merge_classes
      new_elements = []
      get_class_names.each do |target_class|
        elements = @elements.find_all {|el| el.name == target_class }
        complexity = 0
        lines = 0
        defns = []
        elements.each do |el|
          complexity += el.complexity.to_i
          lines += el.lines.to_i
          defns << el.defs
        end

        new_element = {:class_name => target_class,
                       :complexity => complexity,
                       :lines => lines,
                       :methods => defns.flatten.map {|d| d.to_h}}
        new_element[:methods] = new_element[:methods].
                                sort_by {|x| x[:complexity] }.
                                reverse

        new_elements << new_element
      end
      @elements = new_elements if new_elements
    end

    def get_class_names
      class_names = []
      @elements.each do |element|
        unless class_names.include?(element.name)
          class_names << element.name
        end
      end
      class_names
    end

  end

  class Saikuro::ParsingElement
    TYPE_REGEX=/Type:(.*) Name/
    NAME_REGEX=/Name:(.*) Complexity/
    COMPLEXITY_REGEX=/Complexity:(.*) Lines/
    LINES_REGEX=/Lines:(.*)/

    attr_reader :complexity, :lines, :defs, :element_type
    attr_accessor :name

    def initialize(line)
      @line = line
      @element_type = line.match(TYPE_REGEX)[1].strip
      @name = line.match(NAME_REGEX)[1].strip
      @complexity = line.match(COMPLEXITY_REGEX)[1].strip
      @lines = line.match(LINES_REGEX)[1].strip
      @defs = []
    end

    def <<(line)
      @defs << Saikuro::ParsingElement.new(line)
    end

    def to_h
      base = {:name => @name, :complexity => @complexity.to_i, :lines => @lines.to_i}
      unless @defs.empty?
        defs = @defs.map do |my_def|
          my_def = my_def.to_h
          my_def.delete(:defs)
          my_def
        end
        base[:defs] = defs
      end
      return base
    end
  end
end