ripper-tags 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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