ripper-tags 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,8 @@
1
1
  require 'ripper'
2
2
 
3
- class TagRipper < Ripper
3
+ module RipperTags
4
+
5
+ class Parser < Ripper
4
6
  def self.extract(data, file='(eval)')
5
7
  sexp = new(data, file).parse
6
8
  Visitor.new(sexp, file, data).tags
@@ -33,13 +35,14 @@ class TagRipper < Ripper
33
35
  [:defs, receiver && receiver[0], *method]
34
36
  end
35
37
  def on_alias(lhs, rhs)
36
- [:alias, lhs[0], rhs[0], rhs[1]]
38
+ [:alias, lhs[0], rhs[0], rhs[1]] if lhs && rhs
37
39
  end
38
40
  def on_assign(lhs, rhs)
39
41
  return if lhs.nil?
40
42
  return if lhs[0] == :field
41
43
  return if lhs[0] == :aref_field
42
- [:assign, *lhs.flatten(1)]
44
+ lhs, line = lhs
45
+ [:assign, lhs, rhs, line]
43
46
  end
44
47
  def on_sclass(name, body)
45
48
  [:sclass, name && name.flatten(1), *body.compact]
@@ -52,6 +55,7 @@ class TagRipper < Ripper
52
55
  end
53
56
 
54
57
  def on_const_path_ref(a, b)
58
+ return if a.nil? || b.nil?
55
59
  a.flatten!(1)
56
60
  [[a && a[0], b[0]].join('::'), b[1]]
57
61
  end
@@ -61,9 +65,14 @@ class TagRipper < Ripper
61
65
  end
62
66
 
63
67
  def on_command(name, *args)
64
- # if name =~ /^(attr_|alias)/
65
- # [name.to_sym, *args]
66
- # end
68
+ case name[0]
69
+ when "define_method", "alias_method",
70
+ "has_one", "has_many",
71
+ "belongs_to", "has_and_belongs_to_many",
72
+ "scope", "named_scope",
73
+ /^attr_(accessor|reader|writer)$/
74
+ on_method_add_arg([:fcall, name], args[0])
75
+ end
67
76
  end
68
77
  def on_bodystmt(*args)
69
78
  args
@@ -78,6 +87,20 @@ class TagRipper < Ripper
78
87
  end
79
88
  alias on_if_mod on_unless_mod
80
89
 
90
+ def on_dyna_symbol(*args)
91
+ if args.length == 1 && args[0]
92
+ [args[0], lineno]
93
+ end
94
+ end
95
+
96
+ def on_tstring_content(str)
97
+ str
98
+ end
99
+
100
+ def on_xstring_add(first, arg)
101
+ arg if first.nil?
102
+ end
103
+
81
104
  def on_var_ref(*args)
82
105
  on_vcall(*args) || args
83
106
  end
@@ -87,7 +110,84 @@ class TagRipper < Ripper
87
110
  end
88
111
 
89
112
  def on_call(lhs, op, rhs)
90
- [:call, lhs && lhs[0], rhs && rhs[0], rhs[1]]
113
+ return unless lhs && rhs
114
+ arg = block = nil
115
+ [:call, lhs[0], rhs[0], arg, block]
116
+ end
117
+
118
+ def on_method_add_arg(call, args)
119
+ call_name = call && call[0]
120
+ first_arg = args && :args == args[0] && args[1]
121
+
122
+ if :call == call_name && first_arg
123
+ if args.length == 2
124
+ # augment call if a single argument was used
125
+ call = call.dup
126
+ call[3] = args[1]
127
+ end
128
+ call
129
+ elsif :fcall == call_name && first_arg
130
+ name, line = call[1]
131
+ case name
132
+ when "alias_method"
133
+ [:alias, args[1][0], args[2][0], line] if args[1] && args[2]
134
+ when "define_method"
135
+ [:def, args[1][0], line]
136
+ when "scope", "named_scope"
137
+ [:rails_def, :scope, args[1][0], line]
138
+ when /^attr_(accessor|reader|writer)$/
139
+ gen_reader = $1 != 'writer'
140
+ gen_writer = $1 != 'reader'
141
+ args[1..-1].inject([]) do |gen, arg|
142
+ gen << [:def, arg[0], line] if gen_reader
143
+ gen << [:def, "#{arg[0]}=", line] if gen_writer
144
+ gen
145
+ end
146
+ when "has_many", "has_and_belongs_to_many"
147
+ a = args[1][0]
148
+ kind = name.to_sym
149
+ gen = []
150
+ gen << [:rails_def, kind, a, line]
151
+ gen << [:rails_def, kind, "#{a}=", line]
152
+ if (sing = a.chomp('s')) != a
153
+ # poor man's singularize
154
+ gen << [:rails_def, kind, "#{sing}_ids", line]
155
+ gen << [:rails_def, kind, "#{sing}_ids=", line]
156
+ end
157
+ gen
158
+ when "belongs_to", "has_one"
159
+ a = args[1][0]
160
+ kind = name.to_sym
161
+ %W[ #{a} #{a}= build_#{a} create_#{a} create_#{a}! ].inject([]) do |gen, ident|
162
+ gen << [:rails_def, kind, ident, line]
163
+ end
164
+ end
165
+ else
166
+ super
167
+ end
168
+ end
169
+
170
+ # handle `Class.new arg` call without parens
171
+ def on_command_call(*args)
172
+ if args.last && :args == args.last[0]
173
+ args_add = args.pop
174
+ call = on_call(*args)
175
+ on_method_add_arg(call, args_add)
176
+ else
177
+ super
178
+ end
179
+ end
180
+
181
+ def on_fcall(*args)
182
+ [:fcall, *args]
183
+ end
184
+
185
+ def on_args_add(sub, arg)
186
+ if sub
187
+ sub + [arg]
188
+ else
189
+ [:args, arg].compact
190
+ end
91
191
  end
92
192
 
93
193
  def on_do_block(*args)
@@ -95,14 +195,22 @@ class TagRipper < Ripper
95
195
  end
96
196
 
97
197
  def on_method_add_block(method, body)
98
- return unless method and body
99
- if method[2] == 'class_eval'
198
+ return unless method
199
+ if %w[class_eval module_eval].include?(method[2]) && body
100
200
  [:class_eval, [
101
201
  method[1].is_a?(Array) ? method[1][0] : method[1],
102
202
  method[3]
103
203
  ], body.last]
204
+ elsif :call == method[0] && body
205
+ # augment the `Class.new/Struct.new` call with associated block
206
+ call = method.dup
207
+ call[4] = body.last
208
+ call
209
+ else
210
+ super
104
211
  end
105
212
  end
213
+ end
106
214
 
107
215
  class Visitor
108
216
  attr_reader :tags
@@ -117,14 +225,14 @@ class TagRipper < Ripper
117
225
  end
118
226
 
119
227
  def emit_tag(kind, line, opts={})
120
- @tags << opts.merge(
228
+ @tags << {
121
229
  :kind => kind.to_s,
122
230
  :line => line,
123
231
  :language => 'Ruby',
124
232
  :path => @path,
125
233
  :pattern => @lines[line-1].chomp,
126
234
  :access => @current_access
127
- ).delete_if{ |k,v| v.nil? }
235
+ }.update(opts).delete_if{ |k,v| v.nil? }
128
236
  end
129
237
 
130
238
  def process(sexp)
@@ -153,6 +261,7 @@ class TagRipper < Ripper
153
261
  superclass_name = superclass[0] == :call ?
154
262
  superclass[1] :
155
263
  superclass[0]
264
+ superclass_name = nil unless superclass_name =~ /^[A-Z]/
156
265
  end
157
266
  full_name = @namespace.join('::')
158
267
  parts = full_name.split('::')
@@ -170,7 +279,7 @@ class TagRipper < Ripper
170
279
  @namespace.pop
171
280
  end
172
281
 
173
- def on_module(name, body)
282
+ def on_module(name, body = nil)
174
283
  on_module_or_class(:module, name, nil, body)
175
284
  end
176
285
 
@@ -182,9 +291,16 @@ class TagRipper < Ripper
182
291
  def on_protected() @current_access = 'protected' end
183
292
  def on_public() @current_access = 'public' end
184
293
 
185
- def on_assign(name, line)
294
+ def on_assign(name, rhs, line)
186
295
  return unless name =~ /^[A-Z]/
187
296
 
297
+ if rhs && :call == rhs[0] && rhs[1] && "#{rhs[1][0]}.#{rhs[2]}" =~ /^(Class|Module|Struct)\.new$/
298
+ kind = $1 == 'Module' ? :module : :class
299
+ superclass = $1 == 'Class' ? rhs[3] : nil
300
+ superclass.flatten! if superclass
301
+ return on_module_or_class(kind, [name, line], superclass, rhs[4])
302
+ end
303
+
188
304
  emit_tag :constant, line,
189
305
  :name => name,
190
306
  :full_name => (@namespace + [name]).join('::'),
@@ -219,6 +335,16 @@ class TagRipper < Ripper
219
335
  :class => ns.join('::')
220
336
  end
221
337
 
338
+ def on_rails_def(kind, name, line)
339
+ ns = (@namespace.empty?? 'Object' : @namespace.join('::'))
340
+
341
+ emit_tag kind, line,
342
+ :language => 'Rails',
343
+ :name => name,
344
+ :full_name => "#{ns}.#{name}",
345
+ :class => ns
346
+ end
347
+
222
348
  def on_sclass(name, body)
223
349
  name, line = *name
224
350
  @namespace << name unless name == 'self'
@@ -241,5 +367,7 @@ class TagRipper < Ripper
241
367
  end
242
368
  alias on_aref_field on_call
243
369
  alias on_field on_call
370
+ alias on_fcall on_call
371
+ alias on_args on_call
244
372
  end
245
373
  end
@@ -0,0 +1,74 @@
1
+ require 'ripper-tags/default_formatter'
2
+
3
+ module RipperTags
4
+ class VimFormatter < DefaultFormatter
5
+ def header
6
+ <<-EOC
7
+ !_TAG_FILE_FORMAT\t2\t/extended format; --format=1 will not append ;" to lines/
8
+ !_TAG_FILE_SORTED\t1\t/0=unsorted, 1=sorted, 2=foldcase/
9
+ EOC
10
+ end
11
+
12
+ # prepend header and sort lines before closing output
13
+ def with_output
14
+ super do |out|
15
+ out.puts header
16
+ @queued_write = []
17
+ yield out
18
+ @queued_write.sort.each do |line|
19
+ out.puts(line)
20
+ end
21
+ end
22
+ end
23
+
24
+ def write(tag, out)
25
+ @queued_write << format(tag)
26
+ end
27
+
28
+ def display_constant(const)
29
+ const.to_s.gsub('::', '.')
30
+ end
31
+
32
+ def display_pattern(tag)
33
+ tag.fetch(:pattern).to_s.gsub('\\','\\\\\\\\').gsub('/','\\/')
34
+ end
35
+
36
+ def display_class(tag)
37
+ if tag[:class]
38
+ "\tclass:%s" % display_constant(tag[:class])
39
+ else
40
+ ""
41
+ end
42
+ end
43
+
44
+ def display_inheritance(tag)
45
+ if tag[:inherits] && 'class' == tag[:kind]
46
+ "\tinherits:%s" % display_constant(tag[:inherits])
47
+ else
48
+ ""
49
+ end
50
+ end
51
+
52
+ def display_kind(tag)
53
+ case tag.fetch(:kind)
54
+ when 'method' then 'f'
55
+ when 'singleton method' then 'F'
56
+ when 'constant' then 'C'
57
+ when 'scope', 'belongs_to', 'has_one', 'has_many', 'has_and_belongs_to_many'
58
+ 'F'
59
+ else tag[:kind].slice(0,1)
60
+ end
61
+ end
62
+
63
+ def format(tag)
64
+ "%s\t%s\t/^%s$/;\"\t%s%s%s" % [
65
+ tag.fetch(:name),
66
+ relative_path(tag),
67
+ display_pattern(tag),
68
+ display_kind(tag),
69
+ display_class(tag),
70
+ display_inheritance(tag),
71
+ ]
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,120 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+ require 'ripper-tags/parser'
4
+ require 'ripper-tags/data_reader'
5
+ require 'ripper-tags/default_formatter'
6
+ require 'ripper-tags/emacs_formatter'
7
+ require 'ripper-tags/vim_formatter'
8
+ require 'ripper-tags/json_formatter'
9
+
10
+ module RipperTags
11
+ def self.version() "0.1.2" end
12
+
13
+ def self.default_options
14
+ OpenStruct.new \
15
+ :format => nil,
16
+ :tag_file_name => "./tags",
17
+ :tag_relative => nil,
18
+ :debug => false,
19
+ :verbose_debug => false,
20
+ :verbose => false,
21
+ :force => false,
22
+ :files => %w[.],
23
+ :recursive => false,
24
+ :exclude => %w[.git],
25
+ :all_files => false
26
+ end
27
+
28
+ def self.option_parser(options)
29
+ OptionParser.new do |opts|
30
+ opts.banner = "Usage: #{opts.program_name} [options] FILES..."
31
+ opts.version = version
32
+
33
+ opts.separator ""
34
+
35
+ opts.on("-f", "--tag-file (FILE|-)", "File to write tags to (default: `#{options.tag_file_name}')",
36
+ '"-" outputs to standard output') do |fname|
37
+ options.tag_file_name = fname
38
+ end
39
+ opts.on("--tag-relative", "Make file paths relative to the directory of the tag file") do
40
+ options.tag_relative = true
41
+ end
42
+ opts.on("-R", "--recursive", "Descend recursively into subdirectories") do
43
+ options.recursive = true
44
+ end
45
+ opts.on("--exclude PATTERN", "Exclude a file, directory or pattern") do |pattern|
46
+ if pattern.empty?
47
+ options.exclude.clear
48
+ else
49
+ options.exclude << pattern
50
+ end
51
+ end
52
+ opts.on("--all-files", "Parse all files as ruby files, not just `*.rb' ones") do
53
+ options.all_files = true
54
+ end
55
+
56
+ opts.separator " "
57
+
58
+ opts.on("--format (emacs|json|custom)", "Set output format (default: vim)") do |fmt|
59
+ options.format = fmt
60
+ end
61
+ opts.on("-e", "--emacs", "Output Emacs format (default if `--tag-file' is `TAGS')") do
62
+ options.format = "emacs"
63
+ end
64
+
65
+ opts.separator ""
66
+
67
+ opts.on_tail("-d", "--debug", "Output parse tree") do
68
+ options.debug = true
69
+ end
70
+ opts.on_tail("--debug-verbose", "Output parse tree verbosely") do
71
+ options.verbose_debug = true
72
+ end
73
+ opts.on_tail("-V", "--verbose", "Print additional information on stderr") do
74
+ options.verbose = true
75
+ end
76
+ opts.on_tail("--force", "Skip files with parsing errors") do
77
+ options.force = true
78
+ end
79
+ opts.on_tail("-v", "--version", "Print version information") do
80
+ puts opts.ver
81
+ exit
82
+ end
83
+
84
+ yield(opts, options) if block_given?
85
+ end
86
+ end
87
+
88
+ def self.process_args(argv, run = method(:run))
89
+ option_parser(default_options) do |optparse, options|
90
+ file_list = optparse.parse(argv)
91
+ if !file_list.empty? then options.files = file_list
92
+ elsif !options.recursive then abort(optparse.banner)
93
+ end
94
+ options.format ||= File.basename(options.tag_file_name) == "TAGS" ? "emacs" : "vim"
95
+ options.tag_relative = options.format == "emacs" if options.tag_relative.nil?
96
+ return run.call(options)
97
+ end
98
+ end
99
+
100
+ def self.formatter_for(options)
101
+ options.formatter ||
102
+ case options.format
103
+ when "vim" then RipperTags::VimFormatter
104
+ when "emacs" then RipperTags::EmacsFormatter
105
+ when "json" then RipperTags::JSONFormatter
106
+ when "custom" then RipperTags::DefaultFormatter
107
+ else raise ArgumentError, "unknown format: #{options.format.inspect}"
108
+ end.new(options)
109
+ end
110
+
111
+ def self.run(options)
112
+ reader = RipperTags::DataReader.new(options)
113
+ formatter = formatter_for(options)
114
+ formatter.with_output do |out|
115
+ reader.each_tag do |tag|
116
+ formatter.write(tag, out)
117
+ end
118
+ end
119
+ end
120
+ end
data/ripper-tags.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'ripper-tags'
3
- s.version = '0.1.1'
3
+ s.version = '0.1.2'
4
4
 
5
5
  s.summary = 'ctags generator for ruby code'
6
6
  s.description = 'fast, accurate ctags generator for ruby source code using Ripper'
@@ -17,5 +17,7 @@ Gem::Specification.new do |s|
17
17
  s.bindir = 'bin'
18
18
  s.executables << 'ripper-tags'
19
19
 
20
+ s.license = 'MIT'
21
+
20
22
  s.files = `git ls-files`.split("\n")
21
23
  end
File without changes
@@ -0,0 +1,4 @@
1
+ # vi:fenc=latin1
2
+ def encoding
3
+ "This is a test. I�t�rn�ti�n�liz�ti�n\n"
4
+ end
File without changes
File without changes
File without changes
data/test/test_cli.rb ADDED
@@ -0,0 +1,84 @@
1
+ require 'test/unit'
2
+ require 'stringio'
3
+ require 'ripper-tags'
4
+
5
+ class CliTest < Test::Unit::TestCase
6
+ def process_args(argv)
7
+ RipperTags.process_args(argv, lambda {|o| o})
8
+ end
9
+
10
+ def test_empty_args
11
+ err = assert_raise(SystemExit) do
12
+ with_program_name('ripper-tags') do
13
+ capture_stderr do
14
+ RipperTags.process_args([])
15
+ end
16
+ end
17
+ end
18
+ assert_equal "Usage: ripper-tags [options] FILES...", err.message
19
+ end
20
+
21
+ def test_invalid_option
22
+ err = assert_raise(OptionParser::InvalidOption) do
23
+ RipperTags.process_args(%[--moo])
24
+ end
25
+ assert_equal "invalid option: --moo", err.message
26
+ end
27
+
28
+ def test_recurse_defaults_to_current_dir
29
+ options = process_args(%w[-R])
30
+ assert_equal true, options.recursive
31
+ assert_equal %w[.], options.files
32
+ end
33
+
34
+ def test_exclude_add_patterns
35
+ options = process_args(%w[-R --exclude vendor --exclude=bundle/*])
36
+ assert_equal %w[.git vendor bundle/*], options.exclude
37
+ end
38
+
39
+ def test_exclude_clear
40
+ options = process_args(%w[-R --exclude=])
41
+ assert_equal [], options.exclude
42
+ end
43
+
44
+ def test_TAGS_triggers_to_emacs_format
45
+ options = process_args(%w[-f ./TAGS script.rb])
46
+ assert_equal './TAGS', options.tag_file_name
47
+ assert_equal 'emacs', options.format
48
+ end
49
+
50
+ def test_tag_relative_off_by_default
51
+ options = process_args(%w[ -R ])
52
+ assert_equal false, options.tag_relative
53
+ end
54
+
55
+ def test_tag_relative_on
56
+ options = process_args(%w[ -R --tag-relative ])
57
+ assert_equal true, options.tag_relative
58
+ end
59
+
60
+ def test_tag_relative_on_for_emacs
61
+ options = process_args(%w[ -R -e ])
62
+ assert_equal true, options.tag_relative
63
+ end
64
+
65
+ def with_program_name(name)
66
+ old_name = $0
67
+ $0 = name
68
+ begin
69
+ yield
70
+ ensure
71
+ $0 = old_name
72
+ end
73
+ end
74
+
75
+ def capture_stderr
76
+ old_stderr = $stderr
77
+ $stderr = StringIO.new
78
+ begin
79
+ yield
80
+ ensure
81
+ $stderr = old_stderr
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,80 @@
1
+ require 'test/unit'
2
+ require 'ostruct'
3
+ require 'ripper-tags/data_reader'
4
+
5
+ class DataReaderTest < Test::Unit::TestCase
6
+ FIXTURES = File.expand_path('../fixtures', __FILE__)
7
+
8
+ def fixture(path)
9
+ File.join(FIXTURES, path)
10
+ end
11
+
12
+ def find_files(*files)
13
+ opts = files.last.is_a?(Hash) ? files.pop : {}
14
+ options = OpenStruct.new({:files => files, :recursive => true}.merge(opts))
15
+ finder = RipperTags::FileFinder.new(options)
16
+ finder.each_file.map {|f| f.sub("#{FIXTURES}/", '') }
17
+ end
18
+
19
+ def test_encoding
20
+ with_default_encoding('utf-8') do
21
+ options = OpenStruct.new(:files => [fixture('encoding.rb')])
22
+ reader = RipperTags::DataReader.new(options)
23
+ tags = reader.each_tag.to_a
24
+ assert_equal 'Object#encoding', tags[0][:full_name]
25
+ end
26
+ end
27
+
28
+ def test_encoding_non_utf8_default
29
+ with_default_encoding('us-ascii') do
30
+ options = OpenStruct.new(:files => [fixture('encoding.rb')])
31
+ reader = RipperTags::DataReader.new(options)
32
+ tags = reader.each_tag.to_a
33
+ assert_equal 'Object#encoding', tags[0][:full_name]
34
+ end
35
+ end
36
+
37
+ def test_file_finder
38
+ files = find_files(fixture(''), :exclude => %w[_git])
39
+ expected = %w[
40
+ encoding.rb
41
+ very/deep/script.rb
42
+ very/inter.rb
43
+ ]
44
+ assert_equal expected, files
45
+ end
46
+
47
+ def test_file_finder_no_exclude
48
+ files = find_files(fixture(''), :exclude => [])
49
+ assert files.include?('_git/hooks/hook.rb'), files.inspect
50
+ end
51
+
52
+ def test_file_finder_exclude
53
+ files = find_files(fixture(''), :exclude => %w[_git very])
54
+ expected = %w[ encoding.rb ]
55
+ assert_equal expected, files
56
+ end
57
+
58
+ def test_file_finder_exclude_glob
59
+ files = find_files(fixture(''), :exclude => %w[_git very/deep/*])
60
+ expected = %w[
61
+ encoding.rb
62
+ very/inter.rb
63
+ ]
64
+ assert_equal expected, files
65
+ end
66
+
67
+ def with_default_encoding(name)
68
+ if defined?(Encoding)
69
+ old_default = Encoding.default_external
70
+ Encoding.default_external = name
71
+ begin
72
+ yield
73
+ ensure
74
+ Encoding.default_external = old_default
75
+ end
76
+ else
77
+ yield
78
+ end
79
+ end
80
+ end