starscope 1.1.2 → 1.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -2
- data/Gemfile.lock +5 -1
- data/README.md +5 -7
- data/Rakefile +1 -1
- data/bin/starscope +56 -48
- data/doc/DB_FORMAT.md +4 -4
- data/doc/LANGUAGE_SUPPORT.md +8 -4
- data/doc/USER_GUIDE.md +36 -9
- data/lib/starscope/db.rb +150 -265
- data/lib/starscope/export.rb +256 -0
- data/lib/starscope/langs/coffeescript.rb +2 -1
- data/lib/starscope/langs/go.rb +20 -2
- data/lib/starscope/langs/ruby.rb +60 -71
- data/lib/starscope/matcher.rb +1 -1
- data/lib/starscope/output.rb +9 -7
- data/lib/starscope/version.rb +2 -2
- data/starscope.gemspec +2 -1
- data/test/fixtures/db_added_files.json +8 -0
- data/test/fixtures/db_old_extractor.json +15 -0
- data/test/fixtures/db_out_of_date.json +15 -0
- data/test/fixtures/db_removed_files.json +12 -0
- data/test/fixtures/db_up_to_date.json +15 -0
- data/test/fixtures/sample_ruby.rb +21 -21
- data/test/functional/starscope_test.rb +57 -0
- data/test/test_helper.rb +1 -0
- data/test/unit/db_test.rb +141 -0
- data/test/unit/export_test.rb +34 -0
- data/test/unit/langs/golang_test.rb +98 -0
- data/test/unit/langs/ruby_test.rb +66 -0
- data/test/unit/{test_matcher.rb → matcher_test.rb} +6 -6
- data/test/unit/output_test.rb +29 -0
- metadata +41 -15
- data/lib/starscope/record.rb +0 -98
- data/test/functional/test_starscope.rb +0 -35
- data/test/unit/test_db.rb +0 -136
- data/test/unit/test_golang.rb +0 -80
- data/test/unit/test_record.rb +0 -35
- data/test/unit/test_ruby.rb +0 -60
@@ -0,0 +1,256 @@
|
|
1
|
+
module Starscope::Export
|
2
|
+
|
3
|
+
CTAGS_DEFAULT_PATH='tags'
|
4
|
+
CSCOPE_DEFAULT_PATH='cscope.out'
|
5
|
+
|
6
|
+
class UnknownExportFormatError < StandardError; end
|
7
|
+
|
8
|
+
def export(format, path=nil)
|
9
|
+
case format
|
10
|
+
when :ctags
|
11
|
+
path ||= CTAGS_DEFAULT_PATH
|
12
|
+
when :cscope
|
13
|
+
path ||= CSCOPE_DEFAULT_PATH
|
14
|
+
else
|
15
|
+
raise UnknownExportFormatError
|
16
|
+
end
|
17
|
+
|
18
|
+
@output.normal("Exporting to '#{path}' in format '#{format}'...")
|
19
|
+
File.open(path, 'w') do |file|
|
20
|
+
export_to(format, file)
|
21
|
+
end
|
22
|
+
@output.normal("Export complete.")
|
23
|
+
end
|
24
|
+
|
25
|
+
def export_to(format, io)
|
26
|
+
case format
|
27
|
+
when :ctags
|
28
|
+
export_ctags(io)
|
29
|
+
when :cscope
|
30
|
+
export_cscope(io)
|
31
|
+
else
|
32
|
+
raise UnknownExportFormatError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# cscope has this funky issue where it refuses to recognize function calls that
|
39
|
+
# happen outside of a function definition - this isn't an issue in C, where all
|
40
|
+
# calls must occur in a function, but in ruby et al. it is perfectly legal to
|
41
|
+
# write normal code outside the "scope" of a function definition - we insert a
|
42
|
+
# fake shim "global" function everywhere we can to work around this
|
43
|
+
CSCOPE_GLOBAL_HACK_START = "\n\t$-\n"
|
44
|
+
CSCOPE_GLOBAL_HACK_STOP = "\n\t}\n"
|
45
|
+
|
46
|
+
def export_ctags(file)
|
47
|
+
file.puts <<END
|
48
|
+
!_TAG_FILE_FORMAT 2 /extended format/
|
49
|
+
!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
|
50
|
+
!_TAG_PROGRAM_AUTHOR Evan Huus /eapache@gmail.com/
|
51
|
+
!_TAG_PROGRAM_NAME Starscope //
|
52
|
+
!_TAG_PROGRAM_URL https://github.com/eapache/starscope //
|
53
|
+
!_TAG_PROGRAM_VERSION #{Starscope::VERSION} //
|
54
|
+
END
|
55
|
+
defs = (@tables[:defs] || {}).sort_by {|x| x[:name][-1].to_s}
|
56
|
+
defs.each do |record|
|
57
|
+
file.puts ctag_line(record, @meta[:files][record[:file]])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# ftp://ftp.eeng.dcu.ie/pub/ee454/cygwin/usr/share/doc/mlcscope-14.1.8/html/cscope.html
|
62
|
+
def export_cscope(file)
|
63
|
+
buf = ""
|
64
|
+
files = []
|
65
|
+
db_by_line().each do |filename, lines|
|
66
|
+
next if lines.empty?
|
67
|
+
|
68
|
+
buf << "\t@#{filename}\n\n"
|
69
|
+
buf << "0 #{CSCOPE_GLOBAL_HACK_START}\n"
|
70
|
+
files << filename
|
71
|
+
func_count = 0
|
72
|
+
|
73
|
+
lines.sort.each do |line_no, records|
|
74
|
+
line = line_for_record(records.first)
|
75
|
+
toks = tokenize_line(line, records)
|
76
|
+
next if toks.empty?
|
77
|
+
|
78
|
+
prev = 0
|
79
|
+
buf << line_no.to_s << " "
|
80
|
+
toks.each do |offset, record|
|
81
|
+
|
82
|
+
next if offset < prev # this probably indicates an extractor bug
|
83
|
+
|
84
|
+
# Don't export nested functions, cscope barfs on them since C doesn't
|
85
|
+
# have them at all. Skipping tokens is easy; since prev isn't updated
|
86
|
+
# they get turned into plain text automatically.
|
87
|
+
if record[:type] == :func
|
88
|
+
case record[:tbl]
|
89
|
+
when :defs
|
90
|
+
func_count += 1
|
91
|
+
next unless func_count == 1
|
92
|
+
when :end
|
93
|
+
func_count -= 1
|
94
|
+
next unless func_count == 0
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
buf << CSCOPE_GLOBAL_HACK_STOP if record[:type] == :func && record[:tbl] == :defs
|
99
|
+
buf << cscope_plaintext(line, prev, offset) << "\n"
|
100
|
+
buf << cscope_mark(record[:tbl], record) << record[:key] << "\n"
|
101
|
+
buf << CSCOPE_GLOBAL_HACK_START if record[:type] == :func && record[:tbl] == :end
|
102
|
+
|
103
|
+
prev = offset + record[:key].length
|
104
|
+
|
105
|
+
end
|
106
|
+
buf << cscope_plaintext(line, prev, line.length) << "\n\n"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
buf << "\t@\n"
|
111
|
+
|
112
|
+
header = "cscope 15 #{Dir.pwd} -c "
|
113
|
+
offset = "%010d\n" % (header.length + 11 + buf.bytes.count)
|
114
|
+
|
115
|
+
file.print(header)
|
116
|
+
file.print(offset)
|
117
|
+
file.print(buf)
|
118
|
+
|
119
|
+
file.print("#{@meta[:paths].length}\n")
|
120
|
+
@meta[:paths].each {|p| file.print("#{p}\n")}
|
121
|
+
file.print("0\n")
|
122
|
+
file.print("#{files.length}\n")
|
123
|
+
buf = ""
|
124
|
+
files.each {|f| buf << f + "\n"}
|
125
|
+
file.print("#{buf.length}\n#{buf}")
|
126
|
+
end
|
127
|
+
|
128
|
+
def db_by_line()
|
129
|
+
db = {}
|
130
|
+
@tables.each do |tbl, records|
|
131
|
+
records.each do |record|
|
132
|
+
next if not record[:line_no]
|
133
|
+
record[:tbl] = tbl
|
134
|
+
db[record[:file]] ||= {}
|
135
|
+
db[record[:file]][record[:line_no]] ||= []
|
136
|
+
db[record[:file]][record[:line_no]] << record
|
137
|
+
end
|
138
|
+
end
|
139
|
+
return db
|
140
|
+
end
|
141
|
+
|
142
|
+
def tokenize_line(line, records)
|
143
|
+
toks = {}
|
144
|
+
|
145
|
+
records.each do |record|
|
146
|
+
key = record[:name][-1].to_s
|
147
|
+
|
148
|
+
# use the column if we have it, otherwise fall back to scanning
|
149
|
+
index = record[:col] || line.index(key)
|
150
|
+
|
151
|
+
# keep scanning if our current index doesn't actually match the key, or if
|
152
|
+
# either the preceeding or succeeding character is a word character
|
153
|
+
# (meaning we've accidentally matched the middle of some other token)
|
154
|
+
while !index.nil? &&
|
155
|
+
((line[index, key.length] != key) ||
|
156
|
+
(index > 0 && line[index-1] =~ /\w/) ||
|
157
|
+
(index+key.length < line.length && line[index+key.length] =~ /\w/))
|
158
|
+
index = line.index(key, index+1)
|
159
|
+
end
|
160
|
+
|
161
|
+
next if index.nil?
|
162
|
+
|
163
|
+
# Strip trailing non-word characters, otherwise cscope barfs on
|
164
|
+
# function names like `include?`
|
165
|
+
if key =~ /^\W*$/
|
166
|
+
next unless [:defs, :end].include?(record[:tbl])
|
167
|
+
else
|
168
|
+
key.sub!(/\W+$/, '')
|
169
|
+
end
|
170
|
+
|
171
|
+
record[:key] = key
|
172
|
+
toks[index] = record
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
return toks.sort
|
177
|
+
end
|
178
|
+
|
179
|
+
def cscope_plaintext(line, start, stop)
|
180
|
+
ret = line.slice(start, stop-start)
|
181
|
+
ret.lstrip! if start == 0
|
182
|
+
ret.rstrip! if stop == line.length
|
183
|
+
ret.gsub(/\s+/, ' ')
|
184
|
+
rescue ArgumentError
|
185
|
+
# invalid utf-8 byte sequence in the line, oh well
|
186
|
+
line
|
187
|
+
end
|
188
|
+
|
189
|
+
def cscope_mark(tbl, rec)
|
190
|
+
case tbl
|
191
|
+
when :end
|
192
|
+
case rec[:type]
|
193
|
+
when :func
|
194
|
+
ret = "}"
|
195
|
+
else
|
196
|
+
return ""
|
197
|
+
end
|
198
|
+
when :file
|
199
|
+
ret = "@"
|
200
|
+
when :defs
|
201
|
+
case rec[:type]
|
202
|
+
when :func
|
203
|
+
ret = "$"
|
204
|
+
when :class, :module
|
205
|
+
ret = "c"
|
206
|
+
when :type
|
207
|
+
ret = "t"
|
208
|
+
else
|
209
|
+
ret = "g"
|
210
|
+
end
|
211
|
+
when :calls
|
212
|
+
ret = "`"
|
213
|
+
when :requires
|
214
|
+
ret = "~\""
|
215
|
+
when :imports
|
216
|
+
ret = "~<"
|
217
|
+
when :assigns
|
218
|
+
ret = "="
|
219
|
+
else
|
220
|
+
return ""
|
221
|
+
end
|
222
|
+
|
223
|
+
return "\t" + ret
|
224
|
+
end
|
225
|
+
|
226
|
+
def ctag_line(rec, file)
|
227
|
+
ret = "#{rec[:name][-1]}\t#{rec[:file]}\t/^#{line_for_record(rec)}$/"
|
228
|
+
|
229
|
+
ext = ctag_ext_tags(rec, file)
|
230
|
+
if not ext.empty?
|
231
|
+
ret << ";\""
|
232
|
+
ext.sort.each do |k, v|
|
233
|
+
ret << "\t#{k}:#{v}"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
ret
|
238
|
+
end
|
239
|
+
|
240
|
+
def ctag_ext_tags(rec, file)
|
241
|
+
tag = {}
|
242
|
+
|
243
|
+
# these extensions are documented at http://ctags.sourceforge.net/FORMAT
|
244
|
+
case rec[:type]
|
245
|
+
when :func
|
246
|
+
tag["kind"] = "f"
|
247
|
+
when :module, :class
|
248
|
+
tag["kind"] = "c"
|
249
|
+
end
|
250
|
+
|
251
|
+
tag["language"] = file[:lang]
|
252
|
+
|
253
|
+
tag
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
data/lib/starscope/langs/go.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
module
|
1
|
+
module Starscope::Lang
|
2
2
|
module Go
|
3
|
+
VERSION = 1
|
4
|
+
|
3
5
|
FUNC_CALL = /([\w\.]*?\w)\(/
|
4
6
|
END_OF_BLOCK = /^\s*\}\s*$/
|
5
7
|
END_OF_GROUP = /^\s*\)\s*$/
|
@@ -43,7 +45,8 @@ module StarScope::Lang
|
|
43
45
|
# strip string literals like "foo" unless they're part of an import
|
44
46
|
pos = 0
|
45
47
|
while match = STRING_LITERAL.match(line[pos..-1])
|
46
|
-
|
48
|
+
eos = find_end_of_string(line, match.begin(0))
|
49
|
+
line = line[0..match.begin(0)] + line[eos..-1]
|
47
50
|
pos += match.begin(0) + 2
|
48
51
|
end
|
49
52
|
end
|
@@ -188,5 +191,20 @@ module StarScope::Lang
|
|
188
191
|
stack.pop
|
189
192
|
scope.pop
|
190
193
|
end
|
194
|
+
|
195
|
+
def self.find_end_of_string(line, start)
|
196
|
+
escape = false
|
197
|
+
(start+1...line.length).each do |i|
|
198
|
+
if escape
|
199
|
+
escape = false
|
200
|
+
elsif line[i].chr == '\\'
|
201
|
+
escape = true
|
202
|
+
elsif line[i].chr == '"'
|
203
|
+
return i
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
return line.length
|
208
|
+
end
|
191
209
|
end
|
192
210
|
end
|
data/lib/starscope/langs/ruby.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
require "parser/current"
|
2
2
|
|
3
|
-
module
|
3
|
+
module Starscope::Lang
|
4
4
|
module Ruby
|
5
|
+
VERSION = 1
|
6
|
+
|
5
7
|
def self.match_file(name)
|
6
8
|
return true if name.end_with?(".rb")
|
7
9
|
File.open(name) do |f|
|
@@ -18,97 +20,84 @@ module StarScope::Lang
|
|
18
20
|
ast = Parser::CurrentRuby.parse_file(file)
|
19
21
|
rescue
|
20
22
|
else
|
21
|
-
|
23
|
+
extract_tree(ast, [], &block) if not ast.nil?
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
25
27
|
private
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
@ast = ast
|
30
|
-
@scope = []
|
31
|
-
end
|
29
|
+
def self.extract_tree(tree, scope, &block)
|
30
|
+
extract_node(tree, scope, &block)
|
32
31
|
|
33
|
-
|
34
|
-
|
32
|
+
new_scope = []
|
33
|
+
if [:class, :module].include? tree.type
|
34
|
+
new_scope = scoped_name(tree.children[0], scope)
|
35
|
+
scope += new_scope
|
35
36
|
end
|
36
37
|
|
37
|
-
|
38
|
+
tree.children.each {|node| extract_tree(node, scope, &block) if node.is_a? AST::Node}
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
new_scope = []
|
43
|
-
if [:class, :module].include? tree.type
|
44
|
-
new_scope = scoped_name(tree.children[0])
|
45
|
-
@scope += new_scope
|
46
|
-
end
|
47
|
-
|
48
|
-
tree.children.each {|node| extract_tree node, &block if node.is_a? AST::Node}
|
49
|
-
|
50
|
-
@scope.pop(new_scope.count)
|
51
|
-
end
|
40
|
+
scope.pop(new_scope.count)
|
41
|
+
end
|
52
42
|
|
53
|
-
|
54
|
-
|
43
|
+
def self.extract_node(node, scope)
|
44
|
+
loc = node.location
|
55
45
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
46
|
+
case node.type
|
47
|
+
when :send
|
48
|
+
name = scoped_name(node, scope)
|
49
|
+
yield :calls, name, :line_no => loc.line, :col => loc.column
|
60
50
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
51
|
+
if name.last.to_s =~ /\w+=$/
|
52
|
+
name[-1] = name.last.to_s.chop.to_sym
|
53
|
+
yield :assigns, name, :line_no => loc.line, :col => loc.column
|
54
|
+
elsif node.children[0].nil? and node.children[1] == :require and node.children[2].type == :str
|
55
|
+
yield :requires, node.children[2].children[0].split("/"),
|
56
|
+
:line_no => loc.line, :col => loc.column
|
57
|
+
end
|
68
58
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
59
|
+
when :def
|
60
|
+
yield :defs, scope + [node.children[0]],
|
61
|
+
:line_no => loc.line, :type => :func, :col => loc.name.column
|
62
|
+
yield :end, :end, :line_no => loc.end.line, :type => :func, :col => loc.end.column
|
73
63
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
64
|
+
when :defs
|
65
|
+
yield :defs, scope + [node.children[1]],
|
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
|
78
68
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
69
|
+
when :module, :class
|
70
|
+
yield :defs, scope + scoped_name(node.children[0], scope),
|
71
|
+
:line_no => loc.line, :type => node.type, :col => loc.name.column
|
72
|
+
yield :end, :end, :line_no => loc.end.line, :type => node.type, :col => loc.end.column
|
83
73
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
74
|
+
when :casgn
|
75
|
+
fqn = scoped_name(node, scope)
|
76
|
+
yield :assigns, fqn, :line_no => loc.line, :col => loc.name.column
|
77
|
+
yield :defs, fqn, :line_no => loc.line, :col => loc.name.column
|
88
78
|
|
89
|
-
|
90
|
-
|
91
|
-
end
|
79
|
+
when :lvasgn, :ivasgn, :cvasgn, :gvasgn
|
80
|
+
yield :assigns, scope + [node.children[0]], :line_no => loc.line, :col => loc.name.column
|
92
81
|
end
|
82
|
+
end
|
93
83
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
end
|
84
|
+
def self.scoped_name(node, scope)
|
85
|
+
if node.type == :block
|
86
|
+
scoped_name(node.children[0], scope)
|
87
|
+
elsif [:lvar, :ivar, :cvar, :gvar, :const, :send, :casgn].include? node.type
|
88
|
+
if node.children[0].is_a? Symbol
|
89
|
+
[node.children[0]]
|
90
|
+
elsif node.children[0].is_a? AST::Node
|
91
|
+
scoped_name(node.children[0], scope) << node.children[1]
|
92
|
+
elsif node.children[0].nil?
|
93
|
+
if node.type == :const
|
94
|
+
[node.children[1]]
|
95
|
+
else
|
96
|
+
scope + [node.children[1]]
|
108
97
|
end
|
109
|
-
else
|
110
|
-
[node.type]
|
111
98
|
end
|
99
|
+
else
|
100
|
+
[node.type]
|
112
101
|
end
|
113
102
|
end
|
114
103
|
end
|