starscope 1.0.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +0 -1
- data/CHANGELOG.md +24 -1
- data/Gemfile.lock +6 -6
- data/README.md +8 -0
- data/Rakefile +1 -2
- data/bin/starscope +42 -24
- data/doc/DB_FORMAT.md +42 -0
- data/doc/LANGUAGE_SUPPORT.md +80 -0
- data/doc/USER_GUIDE.md +135 -0
- data/lib/starscope/db.rb +135 -83
- data/lib/starscope/langs/coffeescript.rb +1 -1
- data/lib/starscope/langs/go.rb +3 -0
- data/lib/starscope/langs/ruby.rb +11 -11
- data/lib/starscope/output.rb +16 -9
- data/lib/starscope/version.rb +1 -1
- data/test/fixtures/db_old.json.gz +0 -0
- data/test/{files → fixtures}/empty +0 -0
- data/test/{files → fixtures}/sample_golang.go +4 -0
- data/test/{files → fixtures}/sample_ruby.rb +0 -0
- data/test/functional/test_starscope.rb +35 -0
- data/test/test_helper.rb +5 -3
- data/test/{lib → unit}/test_db.rb +25 -18
- data/test/{lib → unit}/test_golang.rb +2 -0
- data/test/{lib → unit}/test_matcher.rb +0 -0
- data/test/{lib → unit}/test_record.rb +2 -2
- data/test/{lib → unit}/test_ruby.rb +0 -0
- metadata +36 -52
- data/test/files/db_old.json.gz +0 -0
- data/test/lib/test_starscope.rb +0 -33
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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(
|
27
|
-
@output = StarScope::Output.new(
|
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
|
-
|
81
|
-
|
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
|
-
|
104
|
-
|
107
|
+
changes = @meta[:files].keys.group_by {|name| file_changed(name)}
|
108
|
+
changes[:modified] ||= []
|
109
|
+
changes[:deleted] ||= []
|
105
110
|
|
106
|
-
@
|
107
|
-
|
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
|
-
|
122
|
-
|
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
|
-
@
|
126
|
-
remove_files(
|
127
|
-
|
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
|
-
|
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
|
-
|
215
|
-
|
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.
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
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.
|
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
|
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]
|
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
|
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
|
396
|
+
elsif @meta[:files][name][:last_updated] < File.mtime(name).to_i
|
345
397
|
:modified
|
346
398
|
else
|
347
399
|
:unchanged
|
data/lib/starscope/langs/go.rb
CHANGED
@@ -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)
|
data/lib/starscope/langs/ruby.rb
CHANGED
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
82
|
-
yield :defs, fqn, :line_no => loc.
|
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.
|
85
|
+
yield :assigns, @scope + [node.children[0]], :line_no => loc.line, :col => loc.name.column
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
data/lib/starscope/output.rb
CHANGED
@@ -4,14 +4,13 @@ class StarScope::Output
|
|
4
4
|
|
5
5
|
PBAR_FORMAT = '%t: %c/%C %E ||%b>%i||'
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@
|
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 @
|
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
|
-
|
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
|
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
|
data/lib/starscope/version.rb
CHANGED