Skip to content

Instantly share code, notes, and snippets.

@tmm1
Last active December 10, 2015 07:58
Show Gist options
  • Save tmm1/4404083 to your computer and use it in GitHub Desktop.
Save tmm1/4404083 to your computer and use it in GitHub Desktop.
Simple ctags generator for ruby code using melbourne.
$ ruby tags.rb
    5	def	Rubinius.ruby18?
    6	class	SyntaxError
    7	def	SyntaxError.from
   13	class	Rubinius::AST::Node
   14	def	Rubinius::AST::Node#visit
   26	def	Rubinius::AST::Node#full_name
   31	class	Visitor
   32	def	Visitor#initialize
   46	def	Visitor#emit_tag
   54	def	Visitor#s_class
   62	def	Visitor#s_class_end
   69	def	Visitor#class
   81	def	Visitor#class_end
   87	def	Visitor#define
   98	def	Visitor#define_end
  102	def	Visitor#define_singleton
  106	def	Visitor#define_singleton_end
  111	def	Visitor#constant_assignment

$ ruby tags.rb /usr/lib/ruby/1.8/timeout.rb
   30	module	Timeout
   35	class	Timeout::Error
   37	class	Timeout::ExitException
   40	const	Timeout::THIS_FILE
   41	const	Timeout::CALLER_OFFSET
   52	def	Timeout#timeout
  100	def	Object#timeout
  108	const	TimeoutError

$ ruby tags.rb /usr/lib/ruby/1.8/timeout.rb --ctags
!_TAG_FILE_FORMAT	2	/extended format/
!_TAG_FILE_SORTED	0	/0=unsorted, 1=sorted, 2=foldcase/
Timeout	/usr/lib/ruby/1.8/timeout.rb	/^module Timeout$/;"	m
Error	/usr/lib/ruby/1.8/timeout.rb	/^  class Error < Interrupt$/;"	c	class:Timeout	inherits:Interrupt
ExitException	/usr/lib/ruby/1.8/timeout.rb	/^  class ExitException < ::Exception # :nodoc:$/;"	c	class:Timeout	inherits:Exception
THIS_FILE	/usr/lib/ruby/1.8/timeout.rb	/^  THIS_FILE = \/\\A#{Regexp.quote(__FILE__)}:\/o$/;"	C
CALLER_OFFSET	/usr/lib/ruby/1.8/timeout.rb	/^  CALLER_OFFSET = ((c = caller[0]) && THIS_FILE =~ c) ? 1 : 0$/;"	C
timeout	/usr/lib/ruby/1.8/timeout.rb	/^  def timeout(sec, klass = nil)$/;"	f	class:Timeout
timeout	/usr/lib/ruby/1.8/timeout.rb	/^def timeout(n, e = nil, &block) # :nodoc:$/;"	f
TimeoutError	/usr/lib/ruby/1.8/timeout.rb	/^TimeoutError = Timeout::Error # :nodoc:$/;"	C

$ ruby tags.rb /usr/lib/ruby/1.8/timeout.rb --json
{"kind":"module","language":"Ruby","line":30,"full_name":"Timeout","name":"Timeout","path":"/usr/lib/ruby/1.8/timeout.rb","pattern":"module Timeout"}
{"kind":"class","language":"Ruby","line":35,"full_name":"Timeout::Error","name":"Error","class":"Timeout","inherits":"Interrupt","path":"/usr/lib/ruby/1.8/timeout.rb","pattern":"  class Error < Interrupt"}
{"kind":"class","language":"Ruby","line":37,"full_name":"Timeout::ExitException","name":"ExitException","class":"Timeout","inherits":"Exception","path":"/usr/lib/ruby/1.8/timeout.rb","pattern":"  class ExitException < ::Exception # :nodoc:"}
{"kind":"constant","language":"Ruby","line":40,"name":"THIS_FILE","full_name":"Timeout::THIS_FILE","path":"/usr/lib/ruby/1.8/timeout.rb","pattern":"  THIS_FILE = /\\A#{Regexp.quote(__FILE__)}:/o"}
{"kind":"constant","language":"Ruby","line":41,"name":"CALLER_OFFSET","full_name":"Timeout::CALLER_OFFSET","path":"/usr/lib/ruby/1.8/timeout.rb","pattern":"  CALLER_OFFSET = ((c = caller[0]) && THIS_FILE =~ c) ? 1 : 0"}
{"kind":"method","language":"Ruby","line":52,"name":"timeout","class":"Timeout","full_name":"Timeout#timeout","path":"/usr/lib/ruby/1.8/timeout.rb","pattern":"  def timeout(sec, klass = nil)"}
{"kind":"method","language":"Ruby","line":100,"name":"timeout","full_name":"Object#timeout","path":"/usr/lib/ruby/1.8/timeout.rb","pattern":"def timeout(n, e = nil, &block) # :nodoc:"}
{"kind":"constant","language":"Ruby","line":108,"name":"TimeoutError","full_name":"TimeoutError","path":"/usr/lib/ruby/1.8/timeout.rb","pattern":"TimeoutError = Timeout::Error # :nodoc:"}
require 'melbourne'
if RUBY_ENGINE == 'ruby'
def Rubinius.ruby18?() true end
class SyntaxError
def self.from(*args)
new(args.first)
end
end
end
class Rubinius::AST::Node
def visit(visitor, parent=nil)
if visitor.respond_to?(self.node_name)
visitor.__send__ self.node_name, self, parent
end
children { |c| c.visit visitor, self }
if visitor.respond_to?("#{self.node_name}_end")
visitor.__send__ "#{self.node_name}_end", self, parent
end
end
def full_name
respond_to?(:parent) && parent ? "#{parent.full_name}.#{name}" : name.to_s
end
end
class Visitor
def initialize(ast = nil)
@namespace = []
@definition = []
@tags = []
ast.visit(self) if ast
end
attr_reader :tags
undef send
###
# helpers
def emit_tag(kind, node, opts={})
opts.delete_if{ |k,v| v.nil? }
@tags << {:kind => kind.to_s, :language => 'Ruby', :line => node.line}.merge(opts)
end
###
# visitor callbacks
def s_class(node, parent)
if node.receiver.node_name != 'self'
@namespace << node.receiver.full_name
end
@prev_is_singleton = @is_singleton
@is_singleton = true
end
def s_class_end(node, parent)
if node.receiver.node_name != 'self'
@namespace.pop
end
@is_singleton = @prev_is_singleton
end
def class(node, parent)
@namespace << node.name.full_name
superclass = node.respond_to?(:superclass) && node.superclass
parts = @namespace.join('.').split('.')
name = parts.pop
emit_tag node.node_name, node,
:full_name => @namespace.join('.').gsub('.','::'),
:name => name,
:class => parts.any? && parts.join('.') || nil,
:inherits => superclass.respond_to?(:name) && superclass.full_name || nil
end
def class_end(node, parent)
@namespace.pop
end
alias module class
alias module_end class_end
def define(node, parent)
@definition << node
return if @definition.size > 1 # skip nested defines
emit_tag @is_singleton ? 'singleton method' : 'method', node,
:name => node.name.to_s,
:class => @namespace.any?? @namespace.join('.') : nil,
:full_name =>
(@namespace.any? ? @namespace.join('.').gsub('.','::') : 'Object') +
(@is_singleton ? ".#{node.name}" : "##{node.name}")
end
def define_end(node, parent)
@definition.pop
end
def define_singleton(node, parent)
s_class(node, parent)
define(node.body, parent)
end
def define_singleton_end(node, parent)
define_end(node.body, parent)
s_class_end(node, parent)
end
def constant_assignment(node, parent)
emit_tag :constant, node,
:name => node.constant.name.to_s,
:full_name => [@namespace + [node.constant.full_name]].join('::')
end
end
if ARGV.delete('--json')
require 'yajl'
json = true
end
if ARGV.delete('--ctags')
ctags = true
puts "!_TAG_FILE_FORMAT\t2\t/extended format/"
puts "!_TAG_FILE_SORTED\t0\t/0=unsorted, 1=sorted, 2=foldcase/"
end
ARGV.unshift __FILE__ if ARGV.empty?
ARGV.each do |file|
begin
if ast = Rubinius::Melbourne.parse_file(file)
lines = File.readlines(file)
v = Visitor.new(ast)
v.tags.each do |tag|
tag[:path] = file
tag[:pattern] = lines[tag[:line]-1].chomp
if json
puts Yajl.dump(tag)
elsif ctags
kwargs = ''
kwargs << "\tclass:#{tag[:class]}" if tag[:class]
kwargs << "\tinherits:#{tag[:inherits]}" if tag[:inherits]
kind = case tag[:kind]
when 'method' then 'f'
when 'singleton method' then 'F'
when 'constant' then 'C'
else tag[:kind].slice(0,1)
end
code = tag[:pattern].gsub('\\','\\\\\\\\').gsub('/','\\/')
puts "%s\t%s\t/^%s$/;\"\t%c%s" % [tag[:name], file, code, kind, kwargs]
else
kind = case tag[:kind]
when /method$/ then 'def'
when /^const/ then 'const'
else tag[:kind]
end
puts "#{tag[:line].to_s.rjust(5)}\t#{kind}\t#{tag[:full_name]}"
end
end
end
rescue SyntaxError
STDERR.puts [$!, file].inspect
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment