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.
@@ -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
@@ -1,5 +1,6 @@
1
- module StarScope::Lang
1
+ module Starscope::Lang
2
2
  module Coffeescript
3
+ VERSION = 0
3
4
 
4
5
  def self.match_file(name)
5
6
  name.end_with?(".coffee")
@@ -1,5 +1,7 @@
1
- module StarScope::Lang
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
- line = line[0...pos] + match.pre_match + "\"\"" + match.post_match
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
@@ -1,7 +1,9 @@
1
1
  require "parser/current"
2
2
 
3
- module StarScope::Lang
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
- Extractor.new(ast).extract &block
23
+ extract_tree(ast, [], &block) if not ast.nil?
22
24
  end
23
25
  end
24
26
 
25
27
  private
26
28
 
27
- class Extractor
28
- def initialize(ast)
29
- @ast = ast
30
- @scope = []
31
- end
29
+ def self.extract_tree(tree, scope, &block)
30
+ extract_node(tree, scope, &block)
32
31
 
33
- def extract(&block)
34
- extract_tree(@ast, &block) if not @ast.nil?
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
- private
38
+ tree.children.each {|node| extract_tree(node, scope, &block) if node.is_a? AST::Node}
38
39
 
39
- def extract_tree(tree, &block)
40
- extract_node tree, &block
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
- def extract_node(node)
54
- loc = node.location
43
+ def self.extract_node(node, scope)
44
+ loc = node.location
55
45
 
56
- case node.type
57
- when :send
58
- name = scoped_name(node)
59
- yield :calls, name, :line_no => loc.line, :col => loc.column
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
- if name.last.to_s =~ /\w+=$/
62
- name[-1] = name.last.to_s.chop.to_sym
63
- yield :assigns, name, :line_no => loc.line, :col => loc.column
64
- elsif node.children[0].nil? and node.children[1] == :require and node.children[2].type == :str
65
- yield :requires, node.children[2].children[0].split("/"),
66
- :line_no => loc.line, :col => loc.column
67
- end
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
- when :def
70
- yield :defs, @scope + [node.children[0]],
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
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
- when :defs
75
- yield :defs, @scope + [node.children[1]],
76
- :line_no => loc.line, :type => :func, :col => loc.name.column
77
- yield :end, :end, :line_no => loc.end.line, :type => :func, :col => loc.end.column
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
- when :module, :class
80
- yield :defs, @scope + scoped_name(node.children[0]),
81
- :line_no => loc.line, :type => node.type, :col => loc.name.column
82
- yield :end, :end, :line_no => loc.end.line, :type => node.type, :col => loc.end.column
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
- when :casgn
85
- fqn = scoped_name(node)
86
- yield :assigns, fqn, :line_no => loc.line, :col => loc.name.column
87
- yield :defs, fqn, :line_no => loc.line, :col => loc.name.column
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
- when :lvasgn, :ivasgn, :cvasgn, :gvasgn
90
- yield :assigns, @scope + [node.children[0]], :line_no => loc.line, :col => loc.name.column
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
- def scoped_name(node)
95
- if node.type == :block
96
- scoped_name(node.children[0])
97
- elsif [:lvar, :ivar, :cvar, :gvar, :const, :send, :casgn].include? node.type
98
- if node.children[0].is_a? Symbol
99
- [node.children[0]]
100
- elsif node.children[0].is_a? AST::Node
101
- scoped_name(node.children[0]) << node.children[1]
102
- elsif node.children[0].nil?
103
- if node.type == :const
104
- [node.children[1]]
105
- else
106
- @scope + [node.children[1]]
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