starscope 1.0.4 → 1.1.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.
data/lib/starscope/db.rb CHANGED
@@ -1,20 +1,27 @@
1
1
  require 'date'
2
2
  require 'oj'
3
+ require 'set'
3
4
  require 'zlib'
4
5
 
5
6
  require 'starscope/matcher'
6
7
  require 'starscope/output'
7
8
  require 'starscope/record'
8
9
 
9
- require 'starscope/langs/coffeescript'
10
- require 'starscope/langs/go'
11
- require 'starscope/langs/ruby'
12
-
13
- LANGS = [
14
- StarScope::Lang::CoffeeScript,
15
- StarScope::Lang::Go,
16
- StarScope::Lang::Ruby
17
- ]
10
+ # cscope has this funky issue where it refuses to recognize function calls that
11
+ # happen outside of a function definition - this isn't an issue in C, where all
12
+ # calls must occur in a function, but in ruby et al. it is perfectly legal to
13
+ # write normal code outside the "scope" of a function definition - we insert a
14
+ # fake shim "global" function everywhere we can to work around this
15
+ CSCOPE_GLOBAL_HACK_START = "\n\t$-\n"
16
+ CSCOPE_GLOBAL_HACK_STOP = "\n\t}\n"
17
+
18
+ # dynamically load all our language extractors
19
+ LANGS = []
20
+ Dir.glob("#{File.dirname(__FILE__)}/langs/*.rb").each do |path|
21
+ require path
22
+ lang = /(\w+)\.rb$/.match(path)[1]
23
+ LANGS << eval("StarScope::Lang::#{lang.capitalize}")
24
+ end
18
25
 
19
26
  class StarScope::DB
20
27
 
@@ -23,8 +30,8 @@ class StarScope::DB
23
30
  class NoTableError < StandardError; end
24
31
  class UnknownDBFormatError < StandardError; end
25
32
 
26
- def initialize(progress, verbose)
27
- @output = StarScope::Output.new(progress, verbose)
33
+ def initialize(output_level)
34
+ @output = StarScope::Output.new(output_level)
28
35
  @meta = {:paths => [], :files => {}, :excludes => [],
29
36
  :version => StarScope::VERSION}
30
37
  @tables = {}
@@ -72,27 +79,24 @@ class StarScope::DB
72
79
  end
73
80
 
74
81
  def add_excludes(paths)
82
+ @output.log("Excluding files in paths #{paths}...")
75
83
  @meta[:paths] -= paths.map {|p| normalize_glob(p)}
76
84
  paths = paths.map {|p| normalize_fnmatch(p)}
77
85
  @meta[:excludes] += paths
78
86
  @meta[:excludes].uniq!
79
87
 
80
- deleted = []
81
- @meta[:files].delete_if do |name, record|
82
- ret = matches_exclude(paths, name)
83
- deleted << name if ret
84
- ret
85
- end
86
- remove_files(deleted)
88
+ excluded = @meta[:files].keys.select {|name| matches_exclude?(paths, name)}
89
+ remove_files(excluded)
87
90
  end
88
91
 
89
92
  def add_paths(paths)
93
+ @output.log("Adding files in paths #{paths}...")
90
94
  @meta[:excludes] -= paths.map {|p| normalize_fnmatch(p)}
91
95
  paths = paths.map {|p| normalize_glob(p)}
92
96
  @meta[:paths] += paths
93
97
  @meta[:paths].uniq!
94
98
  files = Dir.glob(paths).select {|f| File.file? f}
95
- files.delete_if {|f| matches_exclude(@meta[:excludes], f)}
99
+ files.delete_if {|f| matches_exclude?(@meta[:excludes], f)}
96
100
  return if files.empty?
97
101
  @output.new_pbar("Building", files.length)
98
102
  add_new_files(files)
@@ -100,40 +104,33 @@ class StarScope::DB
100
104
  end
101
105
 
102
106
  def update
103
- new_files = (Dir.glob(@meta[:paths]).select {|f| File.file? f}) - @meta[:files].keys
104
- new_files.delete_if {|f| matches_exclude(@meta[:excludes], f)}
107
+ changes = @meta[:files].keys.group_by {|name| file_changed(name)}
108
+ changes[:modified] ||= []
109
+ changes[:deleted] ||= []
105
110
 
106
- @output.new_pbar("Updating", new_files.length + @meta[:files].length)
107
- changed = false
108
- @tables, tmp_tbls = {}, @tables
109
- to_prune = []
110
-
111
- @meta[:files].delete_if do |name, record|
112
- @output.log("Updating `#{name}`")
113
-
114
- event = file_event(name, record)
115
- if event != :unchanged
116
- to_prune << name
117
- changed = true
118
- parse_file(name) if event == :modified
119
- end
111
+ new_files = (Dir.glob(@meta[:paths]).select {|f| File.file? f}) - @meta[:files].keys
112
+ new_files.delete_if {|f| matches_exclude?(@meta[:excludes], f)}
120
113
 
121
- @output.inc_pbar
122
- event == :deleted
114
+ if changes[:deleted].empty? && changes[:modified].empty? && new_files.empty?
115
+ @output.print("No changes detected.")
116
+ return false
123
117
  end
124
118
 
125
- @tables, tmp_tbls = tmp_tbls, @tables
126
- remove_files(to_prune)
127
- merge_db(tmp_tbls)
128
-
119
+ @output.new_pbar("Updating", changes[:modified].length + new_files.length)
120
+ remove_files(changes[:deleted])
121
+ update_files(changes[:modified])
129
122
  add_new_files(new_files)
130
123
  @output.finish_pbar
131
- changed || !new_files.empty?
124
+
125
+ true
132
126
  end
133
127
 
134
128
  def dump_table(table)
135
129
  raise NoTableError if not @tables[table]
130
+
136
131
  puts "== Table: #{table} =="
132
+ puts "No records" if @tables[table].empty?
133
+
137
134
  @tables[table].sort {|a,b|
138
135
  a[:name][-1].to_s.downcase <=> b[:name][-1].to_s.downcase
139
136
  }.each do |record|
@@ -208,36 +205,42 @@ END
208
205
  next if lines.empty?
209
206
 
210
207
  buf << "\t@#{filename}\n\n"
208
+ buf << "0 #{CSCOPE_GLOBAL_HACK_START}\n"
211
209
  files << filename
210
+ func_count = 0
212
211
 
213
212
  lines.sort.each do |line_no, records|
214
- begin
215
- line = records.first[:line].strip.gsub(/\s+/, ' ')
216
- rescue ArgumentError
217
- # invalid utf-8 byte sequence in the line, just do our best
218
- line = records.first[:line]
219
- end
220
-
221
- toks = {}
222
- records.each do |record|
223
- key = record[:name][-1].to_s
224
- index = line.index(key)
225
- while index
226
- toks[index] = record
227
- index = line.index(key, index + 1)
228
- end
229
- end
213
+ line = records.first[:line]
214
+ toks = tokenize_line(line, records)
230
215
  next if toks.empty?
231
216
 
232
217
  prev = 0
233
218
  buf << line_no.to_s << " "
234
- toks.sort.each do |offset, record|
235
- key = record[:name][-1].to_s
236
- buf << line.slice(prev...offset) << "\n"
237
- buf << StarScope::Record.cscope_mark(record[:tbl], record) << key << "\n"
238
- prev = offset + key.length
219
+ toks.each do |offset, record|
220
+
221
+ # Don't export nested functions, cscope barfs on them since C doesn't
222
+ # have them at all. Skipping tokens is easy; since prev isn't updated
223
+ # they get turned into plain text automatically.
224
+ if record[:type] == :func
225
+ case record[:tbl]
226
+ when :defs
227
+ func_count += 1
228
+ next unless func_count == 1
229
+ when :end
230
+ func_count -= 1
231
+ next unless func_count == 0
232
+ end
233
+ end
234
+
235
+ buf << CSCOPE_GLOBAL_HACK_STOP if record[:type] == :func && record[:tbl] == :defs
236
+ buf << cscope_plaintext(line, prev, offset) << "\n"
237
+ buf << StarScope::Record.cscope_mark(record[:tbl], record) << record[:key] << "\n"
238
+ buf << CSCOPE_GLOBAL_HACK_START if record[:type] == :func && record[:tbl] == :end
239
+
240
+ prev = offset + record[:key].length
241
+
239
242
  end
240
- buf << line.slice(prev..-1) << "\n\n"
243
+ buf << cscope_plaintext(line, prev, line.length) << "\n\n"
241
244
  end
242
245
  end
243
246
 
@@ -264,12 +267,27 @@ END
264
267
  def add_new_files(files)
265
268
  files.each do |file|
266
269
  @output.log("Adding `#{file}`")
267
- @meta[:files][file] = {}
268
270
  parse_file(file)
269
271
  @output.inc_pbar
270
272
  end
271
273
  end
272
274
 
275
+ def update_files(files)
276
+ remove_files(files)
277
+ add_new_files(files)
278
+ end
279
+
280
+ def remove_files(files)
281
+ files.each do |file|
282
+ @output.log("Removing `#{file}`")
283
+ @meta[:files].delete(file)
284
+ end
285
+ files = files.to_set
286
+ @tables.each do |name, tbl|
287
+ tbl.delete_if {|val| files.include?(val[:file])}
288
+ end
289
+ end
290
+
273
291
  # File.fnmatch treats a "**" to match files and directories recursively
274
292
  def normalize_fnmatch(path)
275
293
  if path == "."
@@ -307,12 +325,59 @@ END
307
325
  return db
308
326
  end
309
327
 
310
- def matches_exclude(patterns, file)
328
+ def tokenize_line(line, records)
329
+ toks = {}
330
+
331
+ records.each do |record|
332
+ key = record[:name][-1].to_s
333
+
334
+ # use the column if we have it, otherwise fall back to scanning
335
+ index = record[:col] || line.index(key)
336
+
337
+ # keep scanning if our current index doesn't actually match the key, or if
338
+ # either the preceeding or succeeding character is a word character
339
+ # (meaning we've accidentally matched the middle of some other token)
340
+ while !index.nil? &&
341
+ ((line[index, key.length] != key) ||
342
+ (index > 0 && line[index-1] =~ /\w/) ||
343
+ (index+key.length < line.length && line[index+key.length] =~ /\w/))
344
+ index = line.index(key, index+1)
345
+ end
346
+
347
+ next if index.nil?
348
+
349
+ # Strip trailing non-word characters, otherwise cscope barfs on
350
+ # function names like `include?`
351
+ if key =~ /^\W*$/
352
+ next unless [:defs, :end].include?(record[:tbl])
353
+ else
354
+ key.sub!(/\W+$/, '')
355
+ end
356
+
357
+ record[:key] = key
358
+ toks[index] = record
359
+
360
+ end
361
+
362
+ return toks.sort
363
+ end
364
+
365
+ def cscope_plaintext(line, start, stop)
366
+ ret = line.slice(start, stop-start)
367
+ ret.lstrip! if start == 0
368
+ ret.rstrip! if stop == line.length
369
+ ret.gsub(/\s+/, ' ')
370
+ rescue ArgumentError
371
+ # invalid utf-8 byte sequence in the line, oh well
372
+ line
373
+ end
374
+
375
+ def matches_exclude?(patterns, file)
311
376
  patterns.map {|p| File.fnmatch(p, file)}.any?
312
377
  end
313
378
 
314
379
  def parse_file(file)
315
- @meta[:files][file][:last_updated] = File.mtime(file).to_i
380
+ @meta[:files][file] = {:last_updated => File.mtime(file).to_i}
316
381
 
317
382
  LANGS.each do |lang|
318
383
  next if not lang.match_file file
@@ -321,27 +386,14 @@ END
321
386
  @tables[tbl] << StarScope::Record.build(file, name, args)
322
387
  end
323
388
  @meta[:files][file][:lang] = lang.name.split('::').last.to_sym
389
+ return
324
390
  end
325
391
  end
326
392
 
327
- def merge_db(new_tbls)
328
- new_tbls.each do |name, tbl|
329
- @tables[name] ||= []
330
- @tables[name].concat(tbl)
331
- end
332
- end
333
-
334
- def remove_files(files)
335
- check = Hash[files.map {|f| [f, true]}]
336
- @tables.each do |name, tbl|
337
- tbl.delete_if {|val| check[val[:file]]}
338
- end
339
- end
340
-
341
- def file_event(name, record)
393
+ def file_changed(name)
342
394
  if not File.exists?(name) or not File.file?(name)
343
395
  :deleted
344
- elsif record[:last_updated] < File.mtime(name).to_i
396
+ elsif @meta[:files][name][:last_updated] < File.mtime(name).to_i
345
397
  :modified
346
398
  else
347
399
  :unchanged
@@ -1,5 +1,5 @@
1
1
  module StarScope::Lang
2
- module CoffeeScript
2
+ module Coffeescript
3
3
 
4
4
  def self.match_file(name)
5
5
  name.end_with?(".coffee")
@@ -60,6 +60,7 @@ module StarScope::Lang
60
60
  stack.pop
61
61
  when /(.+)\s*=.*/
62
62
  parse_def($1, line_no, scope, &block)
63
+ parse_call(line, line_no, scope, &block)
63
64
  else
64
65
  parse_def(line, line_no, scope, &block)
65
66
  end
@@ -122,10 +123,12 @@ module StarScope::Lang
122
123
  stack.push(:def)
123
124
  when /^var\s+(\w+)\s/
124
125
  yield :defs, scope + [$1], :line_no => line_no
126
+ parse_call(line, line_no, scope, &block)
125
127
  when /^const\s+\(/
126
128
  stack.push(:def)
127
129
  when /^const\s+(\w+)\s/
128
130
  yield :defs, scope + [$1], :line_no => line_no
131
+ parse_call(line, line_no, scope, &block)
129
132
  when /^\s+(.*?) :?=[^=]/
130
133
  $1.split(' ').each do |var|
131
134
  next if CONTROL_KEYS.include?(var)
@@ -55,34 +55,34 @@ module StarScope::Lang
55
55
 
56
56
  case node.type
57
57
  when :send
58
- yield :calls, scoped_name(node), :line_no => loc.expression.line
58
+ yield :calls, scoped_name(node), :line_no => loc.line, :col => loc.column
59
59
  if node.children[0].nil? and node.children[1] == :require and node.children[2].type == :str
60
60
  yield :requires, node.children[2].children[0].split("/"),
61
- :line_no => loc.expression.line
61
+ :line_no => loc.line, :col => loc.column
62
62
  end
63
63
 
64
64
  when :def
65
65
  yield :defs, @scope + [node.children[0]],
66
- :line_no => loc.expression.line, :type => :func
67
- yield :end, :end, :line_no => loc.end.line, :type => :func
66
+ :line_no => loc.line, :type => :func, :col => loc.name.column
67
+ yield :end, :end, :line_no => loc.end.line, :type => :func, :col => loc.end.column
68
68
 
69
69
  when :defs
70
70
  yield :defs, @scope + [node.children[1]],
71
- :line_no => loc.expression.line, :type => :func
72
- yield :end, :end, :line_no => loc.end.line, :type => :func
71
+ :line_no => loc.line, :type => :func, :col => loc.name.column
72
+ yield :end, :end, :line_no => loc.end.line, :type => :func, :col => loc.end.column
73
73
 
74
74
  when :module, :class
75
75
  yield :defs, @scope + scoped_name(node.children[0]),
76
- :line_no => loc.expression.line, :type => node.type
77
- yield :end, :end, :line_no => loc.end.line, :type => node.type
76
+ :line_no => loc.line, :type => node.type, :col => loc.name.column
77
+ yield :end, :end, :line_no => loc.end.line, :type => node.type, :col => loc.end.column
78
78
 
79
79
  when :casgn
80
80
  fqn = scoped_name(node)
81
- yield :assigns, fqn, :line_no => loc.expression.line
82
- yield :defs, fqn, :line_no => loc.expression.line
81
+ yield :assigns, fqn, :line_no => loc.line, :col => loc.name.column
82
+ yield :defs, fqn, :line_no => loc.line, :col => loc.name.column
83
83
 
84
84
  when :lvasgn, :ivasgn, :cvasgn, :gvasgn
85
- yield :assigns, @scope + [node.children[0]], :line_no => loc.expression.line
85
+ yield :assigns, @scope + [node.children[0]], :line_no => loc.line, :col => loc.name.column
86
86
  end
87
87
  end
88
88
 
@@ -4,14 +4,13 @@ class StarScope::Output
4
4
 
5
5
  PBAR_FORMAT = '%t: %c/%C %E ||%b>%i||'
6
6
 
7
- def initialize(progress, verbose)
8
- @progress = progress
9
- @verbose = verbose
7
+ def initialize(level)
8
+ @level = level
10
9
  @pbar = nil
11
10
  end
12
11
 
13
12
  def new_pbar(title, num_items)
14
- if @progress
13
+ if @level != :quiet
15
14
  @pbar = ProgressBar.create(:title => title, :total => num_items,
16
15
  :format => PBAR_FORMAT, :length => 80)
17
16
  end
@@ -22,15 +21,23 @@ class StarScope::Output
22
21
  end
23
22
 
24
23
  def finish_pbar
25
- if @pbar
26
- @pbar.finish
27
- @pbar = nil
28
- end
24
+ @pbar.finish if @pbar
25
+ @pbar = nil
29
26
  end
30
27
 
31
28
  def log(msg)
32
- return if not @verbose
29
+ return if @level != :verbose
30
+ output(msg)
31
+ end
32
+
33
+ def print(msg)
34
+ return if @level == :quiet
35
+ output(msg)
36
+ end
37
+
38
+ private
33
39
 
40
+ def output(msg)
34
41
  if @pbar
35
42
  @pbar.log(msg)
36
43
  else
@@ -1,3 +1,3 @@
1
1
  module StarScope
2
- VERSION = "1.0.4"
2
+ VERSION = "1.1.0"
3
3
  end