fastri 0.1.0.0

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.
@@ -0,0 +1,566 @@
1
+ # Copyright (C) 2006 Mauricio Fernandez <mfp@acm.org>
2
+ #
3
+
4
+ require 'rdoc/ri/ri_cache'
5
+ require 'rdoc/ri/ri_reader'
6
+ require 'fastri/version'
7
+
8
+
9
+ module FastRI
10
+
11
+ # This class provides the same functionality as RiReader, with some
12
+ # improvements:
13
+ # * lower memory consumption
14
+ # * ability to handle information from different sources separately.
15
+ #
16
+ # Some operations can be restricted to a given "scope", that is, a
17
+ # "RI DB directory". This allows you to e.g. look for all the instance methods
18
+ # in String defined by a package.
19
+ #
20
+ # Such operations take a +scope+ argument, which is either an integer which
21
+ # indexes the source in #paths, or a name identifying the source (either
22
+ # "system" or a package name). If <tt>scope == nil</tt>, the information from
23
+ # all sources is merged.
24
+ class RiIndex
25
+ # Redefine RI::MethodEntry#full_name to use the following notation:
26
+ # Namespace::Foo.singleton_method (instead of ::). RiIndex depends on this to
27
+ # tell singleton methods apart.
28
+ class ::RI::MethodEntry # :nodoc:
29
+ remove_method :full_name
30
+ def full_name
31
+ res = @in_class.full_name
32
+ unless res.empty?
33
+ if @is_class_method
34
+ res << "."
35
+ else
36
+ res << "#"
37
+ end
38
+ end
39
+ res << @name
40
+ end
41
+ end
42
+
43
+ class MethodEntry
44
+ attr_reader :full_name, :name, :index, :source_index
45
+
46
+ def initialize(ri_index, fullname, index, source_index)
47
+ # index is the index in ri_index' array
48
+ # source_index either nil (all scopes) or the integer referencing the
49
+ # path (-> we'll do @ri_index.paths[@source_index])
50
+ @ri_index = ri_index
51
+ @full_name = fullname
52
+ @name = fullname[/[.#](.*)$/, 1]
53
+ @index = index
54
+ @source_index = source_index
55
+ end
56
+
57
+ # Returns the "fully resolved" file name of the yaml containing our
58
+ # description.
59
+ def path_name
60
+ prefix = @full_name.split(/::|[#.]/)[0..-2]
61
+ case @source_index
62
+ when nil
63
+ ## we'd like to do
64
+ #@ri_index.source_paths_for(self).map do |path|
65
+ # File.join(File.join(path, *prefix), RI::RiWriter.internal_to_external(@name))
66
+ #end
67
+ # but RI doesn't support merging at the method-level, so
68
+ path = @ri_index.source_paths_for(self).first
69
+ File.join(File.join(path, *prefix),
70
+ RI::RiWriter.internal_to_external(@name) +
71
+ (singleton_method? ? "-c" : "-i" ) + ".yaml")
72
+ else
73
+ path = @ri_index.paths[@source_index]
74
+ File.join(File.join(path, *prefix),
75
+ RI::RiWriter.internal_to_external(@name) +
76
+ (singleton_method? ? "-c" : "-i" ) + ".yaml")
77
+ end
78
+ end
79
+
80
+ def singleton_method?
81
+ /\.[^:]+$/ =~ @full_name
82
+ end
83
+
84
+ def instance_method?
85
+ !singleton_method?
86
+ end
87
+
88
+ # Returns the type of this entry (<tt>:method</tt>).
89
+ def type
90
+ :method
91
+ end
92
+ end
93
+
94
+ class ClassEntry
95
+ attr_reader :full_name, :name, :index, :source_index
96
+
97
+ def initialize(ri_index, fullname, index, source_index)
98
+ @ri_index = ri_index
99
+ @full_name = fullname
100
+ @name = fullname.split(/::/).last
101
+ @index = index
102
+ @source_index = source_index
103
+ end
104
+
105
+ # Returns an array of directory names holding the cdesc-Classname.yaml
106
+ # files.
107
+ def path_names
108
+ prefix = @full_name.split(/::/)
109
+ case @source_index
110
+ when nil
111
+ @ri_index.source_paths_for(self).map{|path| File.join(path, *prefix) }
112
+ else
113
+ [File.join(@ri_index.paths[@source_index], *prefix)]
114
+ end
115
+ end
116
+
117
+ # Returns nested classes and modules matching name (non-recursive).
118
+ def contained_modules_matching(name)
119
+ @ri_index.namespaces_under(self, false, @source_index).select do |x|
120
+ x.name[name]
121
+ end
122
+ end
123
+
124
+ # Returns all nested classes and modules (non-recursive).
125
+ def classes_and_modules
126
+ @ri_index.namespaces_under(self, false, @source_index)
127
+ end
128
+
129
+ # Returns nested class or module named exactly +name+ (non-recursive).
130
+ def contained_class_named(name)
131
+ contained_modules_matching(name).find{|x| x.name == name}
132
+ end
133
+
134
+ # Returns instance or singleton methods matching name (non-recursive).
135
+ def methods_matching(name, is_class_method)
136
+ @ri_index.methods_under(self, false, @source_index).select do |meth|
137
+ meth.name[name] &&
138
+ (is_class_method ? meth.singleton_method? : meth.instance_method?)
139
+ end
140
+ end
141
+
142
+ # Returns instance or singleton methods matching name (recursive).
143
+ def recursively_find_methods_matching(name, is_class_method)
144
+ @ri_index.methods_under(self, true, @source_index).select do |meth|
145
+ meth.name[name] &&
146
+ (is_class_method ? meth.singleton_method? : meth.instance_method?)
147
+ end
148
+ end
149
+
150
+ # Returns all methods, both instance and singleton (non-recursive).
151
+ def all_method_names
152
+ @ri_index.methods_under(self, false, @source_index).map{|meth| meth.full_name}
153
+ end
154
+
155
+ # Returns the type of this entry (<tt>:namespace</tt>).
156
+ def type
157
+ :namespace
158
+ end
159
+ end
160
+
161
+ class TopLevelEntry < ClassEntry
162
+ def methods_matching(name, is_class_method)
163
+ recursively_find_methods_matching(name, is_class_method)
164
+ end
165
+
166
+ def module_named(name)
167
+
168
+ end
169
+ end
170
+
171
+ attr_reader :paths
172
+
173
+ class << self; private :new end
174
+
175
+ def self.new_from_paths(paths = nil)
176
+ obj = new
177
+ obj.rebuild_index(paths)
178
+ obj
179
+ end
180
+
181
+ def self.new_from_IO(anIO)
182
+ obj = new
183
+ obj.load(anIO)
184
+ obj
185
+ end
186
+
187
+ def rebuild_index(paths = nil)
188
+ @paths = paths || RI::Paths::PATH
189
+ @gem_names = paths.map do |p|
190
+ fullp = File.expand_path(p)
191
+ gemname = nil
192
+ Gem.path.each do |gempath|
193
+ re = %r!^#{Regexp.escape(File.expand_path(gempath))}/doc/!
194
+ if re =~ fullp
195
+ gemname = fullp.gsub(re,"")[%r{^[^/]+}]
196
+ break
197
+ end
198
+ end
199
+ gemname ? gemname : "system"
200
+ end
201
+ methods = Hash.new{|h,k| h[k] = []}
202
+ namespaces = methods.clone
203
+ @paths.each_with_index do |path, source_index|
204
+ ri_reader = RI::RiReader.new(RI::RiCache.new(path))
205
+ obtain_classes(ri_reader.top_level_namespace.first).each{|name| namespaces[name] << source_index }
206
+ obtain_methods(ri_reader.top_level_namespace.first).each{|name| methods[name] << source_index }
207
+ end
208
+ @method_array = methods.sort_by{|h,k| h}.map do |name, sources|
209
+ "#{name} #{sources.map{|x| x.to_s}.join(' ')}"
210
+ end
211
+ @namespace_array = namespaces.sort_by{|h,k| h}.map do |name, sources|
212
+ "#{name} #{sources.map{|x| x.to_s}.join(' ')}"
213
+ end
214
+
215
+ =begin
216
+ puts "@method_array: #{@method_array.size}"
217
+ puts "@namespace_array: #{@namespace_array.size}"
218
+ puts @method_array.inject(0){|s,x| s + x.size}
219
+ puts @namespace_array.inject(0){|s,x| s + x.size}
220
+ =end
221
+ end
222
+
223
+ MAGIC = "FastRI index #{FASTRI_INDEX_FORMAT}"
224
+ # Load the index from the given IO.
225
+ # It must contain a textual representation generated by #dump.
226
+ def load(anIO)
227
+ header = anIO.gets
228
+ raise "Invalid format." unless header.chomp == MAGIC
229
+ anIO.gets # discard "Sources:"
230
+ paths = []
231
+ gem_names = []
232
+ until (line = anIO.gets).index("=" * 80) == 0
233
+ gemname, path = line.strip.split(/\s+/)
234
+ paths << path
235
+ gem_names << gemname
236
+ end
237
+ anIO.gets # discard "Namespaces:"
238
+ namespace_array = []
239
+ until (line = anIO.gets).index("=" * 80) == 0
240
+ namespace_array << line
241
+ end
242
+ anIO.gets # discard "Methods:"
243
+ method_array = []
244
+ until (line = anIO.gets).index("=" * 80) == 0
245
+ method_array << line
246
+ end
247
+ @paths = paths
248
+ @gem_names = gem_names
249
+ @namespace_array = namespace_array
250
+ @method_array = method_array
251
+ end
252
+
253
+ # Serializes index to the given IO.
254
+ def dump(anIO)
255
+ anIO.puts MAGIC
256
+ anIO.puts "Sources:"
257
+ @paths.zip(@gem_names).each{|p,g| anIO.puts "%-30s %s" % [g, p]}
258
+ anIO.puts "=" * 80
259
+ anIO.puts "Namespaces:"
260
+ anIO.puts @namespace_array
261
+ anIO.puts "=" * 80
262
+ anIO.puts "Methods:"
263
+ anIO.puts @method_array
264
+ anIO.puts "=" * 80
265
+ end
266
+ #{{{ RiReader compatibility interface
267
+
268
+ # Returns an array with the top level namespace.
269
+ def top_level_namespace(scope = nil)
270
+ [TopLevelEntry.new(self, "", -1, scope ? scope_to_sindex(scope) : nil)]
271
+ end
272
+
273
+ # Returns an array of ClassEntry objects whose names match +target+, and
274
+ # which correspond to the namespaces contained in +namespaces+.
275
+ # +namespaces+ is an array of ClassEntry objects.
276
+ def lookup_namespace_in(target, namespaces)
277
+ result = []
278
+ namespaces.each do |ns|
279
+ result.concat(ns.contained_modules_matching(target))
280
+ end
281
+ result
282
+ end
283
+
284
+ # Returns the ClassDescription associated to the given +full_name+.
285
+ def find_class_by_name(full_name, scope = nil)
286
+ entry = get_entry(@namespace_array, full_name, ClassEntry, scope)
287
+ return nil unless entry && entry.full_name == full_name
288
+ get_class(entry)
289
+ end
290
+
291
+ # Returns the MethodDescription associated to the given +full_name+.
292
+ # Only the first definition is returned when <tt>scope = nil</tt>.
293
+ def find_method_by_name(full_name, scope = nil)
294
+ entry = get_entry(@method_array, full_name, MethodEntry, scope)
295
+ return nil unless entry && entry.full_name == full_name
296
+ get_method(entry)
297
+ end
298
+
299
+ # Returns an array of MethodEntry objects, corresponding to the methods in
300
+ # the ClassEntry objects in the +namespaces+ array.
301
+ def find_methods(name, is_class_method, namespaces)
302
+ result = []
303
+ namespaces.each do |ns|
304
+ result.concat ns.methods_matching(name, is_class_method)
305
+ end
306
+ result
307
+ end
308
+
309
+ # Return the MethodDescription for a given MethodEntry
310
+ # by deserializing the YAML.
311
+ def get_method(method_entry)
312
+ path = method_entry.path_name
313
+ File.open(path) { |f| RI::Description.deserialize(f) }
314
+ end
315
+
316
+ # Return a ClassDescription for a given ClassEntry.
317
+ def get_class(class_entry)
318
+ result = nil
319
+ for path in class_entry.path_names
320
+ path = RI::RiWriter.class_desc_path(path, class_entry)
321
+ desc = File.open(path) {|f| RI::Description.deserialize(f) }
322
+ if result
323
+ result.merge_in(desc)
324
+ else
325
+ result = desc
326
+ end
327
+ end
328
+ result
329
+ end
330
+
331
+ # Return the names of all classes and modules.
332
+ def full_class_names(scope = nil)
333
+ all_entries(@namespace_array, scope)
334
+ end
335
+
336
+ # Return the names of all methods.
337
+ def full_method_names(scope = nil)
338
+ all_entries(@method_array, scope)
339
+ end
340
+
341
+ # Return a list of all classes, modules, and methods.
342
+ def all_names(scope = nil)
343
+ full_class_names(scope).concat(full_method_names(scope))
344
+ end
345
+
346
+ #{{{ New (faster) interface
347
+
348
+ # Returns the number of methods in the index.
349
+ def num_methods
350
+ @method_array.size
351
+ end
352
+
353
+ # Returns the number of namespaces in the index.
354
+ def num_namespaces
355
+ @namespace_array.size
356
+ end
357
+
358
+ # Returns the ClassEntry associated to the given +full_name+.
359
+ def get_class_entry(full_name, scope = nil)
360
+ entry = get_entry(@namespace_array, full_name, ClassEntry, scope)
361
+ return nil unless entry && entry.full_name == full_name
362
+ entry
363
+ end
364
+
365
+ # Returns the MethodEntry associated to the given +full_name+.
366
+ def get_method_entry(full_name, scope = nil)
367
+ entry = get_entry(@method_array, full_name, MethodEntry, scope)
368
+ return nil unless entry && entry.full_name == full_name
369
+ entry
370
+ end
371
+
372
+ # Returns array of ClassEntry objects under class_entry_or_name
373
+ # (either String or ClassEntry) in the hierarchy.
374
+ def namespaces_under(class_entry_or_name, recursive, scope = nil)
375
+ namespaces_under_matching(class_entry_or_name, //, recursive, scope)
376
+ end
377
+
378
+ # Returns array of ClassEntry objects under class_entry_or_name (either
379
+ # String or ClassEntry) in the hierarchy whose +full_name+ matches the given
380
+ # regexp.
381
+ def namespaces_under_matching(class_entry_or_name, regexp, recursive, scope = nil)
382
+ case class_entry_or_name
383
+ when ClassEntry
384
+ class_entry = class_entry_or_name
385
+ when ""
386
+ class_entry = top_level_namespace(scope)[0]
387
+ else
388
+ class_entry = get_entry(@namespace_array, class_entry_or_name, ClassEntry, scope)
389
+ end
390
+ return [] unless class_entry
391
+ ret = []
392
+ re1, re2 = matching_regexps_namespace(class_entry.full_name)
393
+ (class_entry.index+1...@namespace_array.size).each do |i|
394
+ entry = @namespace_array[i]
395
+ break unless re1 =~ entry
396
+ next if !recursive && re2 !~ entry
397
+ full_name = entry[/\S+/]
398
+ next unless regexp =~ full_name
399
+ if scope
400
+ sources = namespace_sources(i)
401
+ if sources.include?(sindex = scope_to_sindex(scope))
402
+ ret << ClassEntry.new(self, full_name, i, sindex)
403
+ end
404
+ else
405
+ ret << ClassEntry.new(self, full_name, i, nil)
406
+ end
407
+ end
408
+ ret
409
+ end
410
+
411
+ # Returns array of MethodEntry objects under class_entry_or_name
412
+ # (either String or ClassEntry) in the hierarchy.
413
+ def methods_under(class_entry_or_name, recursive, scope = nil)
414
+ methods_under_matching(class_entry_or_name, //, recursive, scope)
415
+ end
416
+
417
+ # Returns array of MethodEntry objects under class_entry_or_name (either
418
+ # String or ClassEntry) in the hierarchy whose +full_name+ matches the given
419
+ # regexp.
420
+ def methods_under_matching(class_entry_or_name, regexp, recursive, scope = nil)
421
+ case class_entry_or_name
422
+ when ClassEntry
423
+ full_name = class_entry_or_name.full_name
424
+ else
425
+ full_name = class_entry_or_name
426
+ end
427
+ method_entry = get_entry(@method_array, full_name, MethodEntry)
428
+ return [] unless method_entry
429
+ ret = []
430
+ re1, re2 = matching_regexps_method(full_name)
431
+ (method_entry.index...@method_array.size).each do |i|
432
+ entry = @method_array[i]
433
+ break unless re1 =~ entry
434
+ next if !recursive && re2 !~ entry
435
+ full_name = entry[/\S+/]
436
+ next unless regexp =~ full_name
437
+ if scope
438
+ sources = method_sources(i)
439
+ if sources.include?(sindex = scope_to_sindex(scope))
440
+ ret << MethodEntry.new(self, full_name, i, sindex)
441
+ end
442
+ else
443
+ ret << MethodEntry.new(self, full_name, i, nil)
444
+ end
445
+ end
446
+ ret
447
+ end
448
+
449
+ # Returns array of Strings corresponding to the base directories of all the
450
+ # sources fo the given entry_or_name.
451
+ def source_paths_for(entry_or_name)
452
+ case entry_or_name
453
+ when ClassEntry
454
+ namespace_sources(entry_or_name.index).map{|i| @paths[i] }
455
+ when MethodEntry
456
+ method_sources(entry_or_name.index).map{|i| @paths[i]}
457
+ when nil
458
+ []
459
+ else
460
+ case entry_or_name
461
+ when /[#.]\S+/
462
+ method_entry = get_entry(@method_array, entry_or_name, MethodEntry, nil)
463
+ source_paths_for(method_entry)
464
+ else
465
+ class_entry = get_entry(@namespace_array, entry_or_name, ClassEntry, nil)
466
+ source_paths_for(class_entry)
467
+ end
468
+ end
469
+ end
470
+
471
+ private
472
+ def namespace_sources(index)
473
+ @namespace_array[index][/\S+ (.*)/,1].split(/\s+/).map{|x| x.to_i}
474
+ end
475
+
476
+ def method_sources(index)
477
+ @method_array[index][/\S+ (.*)/,1].split(/\s+/).map{|x| x.to_i}
478
+ end
479
+
480
+ def all_entries(array, scope)
481
+ if scope
482
+ wanted_sidx = scope_to_sindex(scope)
483
+ chosen = array.select{|x| x[/ (.*$)/, 1].split(/\s+/).map{|x| x.to_i}.include? wanted_sidx }
484
+ else
485
+ chosen = array
486
+ end
487
+ chosen.map{|x| x[/(\S+)/]}
488
+ end
489
+
490
+ def matching_regexps_namespace(prefix)
491
+ if prefix.empty?
492
+ [//, /^[^:]+ /]
493
+ else
494
+ [/^#{Regexp.escape(prefix)}/, /^#{Regexp.escape(prefix)}(::|[#.])[^:]+ / ]
495
+ end
496
+ end
497
+
498
+ def matching_regexps_method(prefix)
499
+ if prefix.empty?
500
+ [//, /^[#.] /] # the second should never match
501
+ else
502
+ [/^#{Regexp.escape(prefix)}/, /^#{Regexp.escape(prefix)}([#.])\S+ / ]
503
+ end
504
+ end
505
+
506
+ def scope_to_sindex(scope)
507
+ case scope
508
+ when Integer
509
+ scope
510
+ else
511
+ @gem_names.index(scope)
512
+ end
513
+ end
514
+
515
+ def get_entry(array, fullname, klass, scope = nil)
516
+ index = binary_search(array, fullname)
517
+ return nil unless index
518
+ entry = array[index]
519
+ sources = entry[/\S+ (.*)/,1].split(/\s+/).map{|x| x.to_i}
520
+ if scope
521
+ wanted_sidx = scope_to_sindex(scope)
522
+ return nil unless wanted_sidx
523
+ return nil unless sources.include?(wanted_sidx)
524
+ return klass.new(self, entry[/\S+/], index, wanted_sidx)
525
+ end
526
+ klass.new(self, entry[/\S+/], index, nil)
527
+ end
528
+
529
+ def binary_search(array, name, from = 0, to = array.size - 1)
530
+ middle = (from + to) / 2
531
+ pivot = array[middle][/\S+/]
532
+ if from == to
533
+ if pivot.index(name) == 0
534
+ from
535
+ else
536
+ nil
537
+ end
538
+ elsif name <= pivot
539
+ binary_search(array, name, from, middle)
540
+ elsif name > pivot
541
+ binary_search(array, name, middle+1, to)
542
+ end
543
+ end
544
+
545
+ def obtain_classes(namespace, res = [])
546
+ subnamespaces = namespace.classes_and_modules
547
+ subnamespaces.each do |ns|
548
+ res << ns.full_name
549
+ obtain_classes(ns, res)
550
+ end
551
+ res
552
+ end
553
+
554
+ def obtain_methods(namespace, res = [])
555
+ subnamespaces = namespace.classes_and_modules
556
+ subnamespaces.each do |ns|
557
+ res.concat ns.all_method_names
558
+ obtain_methods(ns, res)
559
+ end
560
+ res
561
+ end
562
+ end
563
+
564
+ end #module FastRI
565
+
566
+ # vi: set sw=2 expandtab: