nadoka/nadoka

View on GitHub
plugins/crubybot.nb

Summary

Maintainability
Test Coverage
# -*-ruby-*-
require 'ffi/clang'
require 'tempfile'

# dependency:
#   * ffi-clang.gem
#   * libclang
#     * lang/clang34 in FreeBSD ports
#     * llvm of homebrew
#
# example setting:
#   {
#     :name => :CRubyBot,
#     :channels => [ "#ruby-ja" ],
#     :ruby_srcdir => '/path/of/ruby/src',
#   }
#
class CRubyBot < Nadoka::NDK_Bot
  def bot_initialize
    if @bot_config.key?(:channels)
      channels = '\A(?:' + @bot_config[:channels].collect{|ch|
        Regexp.quote(ch)
      }.join('|') + ')\z'
      @available_channel = Regexp.compile(channels)
    else
      @available_channel = @bot_config.fetch(:ch, //)
    end
    unless @ruby_srcdir = @bot_config[:ruby_srcdir]
      raise "ruby_srcdir is not specified"
    end
    unless Dir.exist?(@ruby_srcdir)
      raise "ruby_srcdir(#{@ruby_srcdir}) does not exist"
    end
    @ruby_srcdir << '/' if @ruby_srcdir[-1] != '/'
    @translation_unit = nil
    @translation_unit_rev = nil
  end

  def bot_state
    "<#{self.class.to_s}>"
  end

  def translation_unit
    rev = IO.read("#@ruby_srcdir/revision.h").to_s[/\d+/].to_i
    return @translation_unit if @translation_unit_rev == rev
    ary = Dir[File.join @ruby_srcdir, "*.c"]
    f = Tempfile.open(["crubybot-ruby", ".c"])
    ary = Dir[File.join @ruby_srcdir, "*.c"]
    ary << File.join(@ruby_srcdir, "win32/win32.c")
    ary.each do |fn|
      f.puts %[#include "#{fn}"]
    end
    f.flush
    index = FFI::Clang::Index.new
    @translation_unit = index.parse_translation_unit(f.path, "-I#{@ruby_srcdir}/include")
    @translation_unit_rev = rev
    f.close(true)
    @translation_unit
  end

  def find_def(name, kind)
    translation_unit.cursor.visit_children do |cursor, parent|
      if cursor.kind == kind && cursor.definition? && cursor.spelling == name
        loc = cursor.location
        path = loc.file
        if path.start_with?(@ruby_srcdir)
          return [path[@ruby_srcdir.size..-1], loc.line]
        end
      end
      next :recurse
    end
    return nil
  end

  def on_privmsg(client, ch, message)
    return unless @available_channel === ch
    case message
    when /\A\S*(struct|fun\w*)\s+(\w+)/
      kind1 = $1
      name = $2
      kind = case kind1
             when "struct"
               :cursor_struct
             when /\Afun/
               :cursor_function
             end
      path, line = find_def(name, kind)
      return unless path
      send_notice(ch, "crubybot: #{kind1} #{name} at " \
                  "https://github.com/ruby/ruby/blob/trunk/#{path}#L#{line}")
    end
  end
end