solargraph 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,404 @@
1
+ require 'parser/current'
2
+
3
+ module Solargraph
4
+ class CodeMap
5
+ attr_accessor :node
6
+ attr_accessor :api_map
7
+
8
+ include NodeMethods
9
+
10
+ def initialize code: '', filename: nil
11
+ workspace = nil
12
+ unless filename.nil?
13
+ filename.gsub!(File::ALT_SEPARATOR, File::SEPARATOR) unless File::ALT_SEPARATOR.nil?
14
+ workspace = CodeMap.find_workspace(filename)
15
+ end
16
+ @api_map = ApiMap.new(workspace)
17
+ unless workspace.nil?
18
+ files = Dir[File.join workspace, '**', '*.rb']
19
+ files.each { |f|
20
+ unless filename == f
21
+ @api_map.append_file f
22
+ end
23
+ }
24
+ end
25
+
26
+ @code = code.gsub(/\r/, '')
27
+ tries = 0
28
+ # Hide incomplete code to avoid syntax errors
29
+ tmp = "#{code}\nX".gsub(/[\.@]([\s])/, '#\1').gsub(/([\A\s]?)def([\s]*?[\n\Z])/, '\1#ef\2')
30
+ #tmp = code
31
+ begin
32
+ @node, comments = Parser::CurrentRuby.parse_with_comments(tmp)
33
+ @api_map.append_node(@node, comments, filename)
34
+ rescue Parser::SyntaxError => e
35
+ if tries < 10
36
+ tries += 1
37
+ spot = e.diagnostic.location.begin_pos
38
+ if spot == tmp.length
39
+ puts e.message
40
+ tmp = tmp[0..-2] + '#'
41
+ else
42
+ tmp = tmp[0..spot] + '#' + tmp[spot+2..-1].to_s
43
+ end
44
+ retry
45
+ end
46
+ raise e
47
+ end
48
+ end
49
+
50
+ def self.find_workspace filename
51
+ return nil if filename.nil?
52
+ dirname = filename
53
+ lastname = nil
54
+ result = nil
55
+ until dirname == lastname
56
+ if File.file?("#{dirname}/Gemfile")
57
+ result = dirname
58
+ break
59
+ end
60
+ lastname = dirname
61
+ dirname = File.dirname(dirname)
62
+ end
63
+ result ||= File.dirname(filename)
64
+ result.gsub!(File::ALT_SEPARATOR, File::SEPARATOR) unless File::ALT_SEPARATOR.nil?
65
+ result
66
+ end
67
+
68
+ def get_offset line, col
69
+ offset = 0
70
+ if line > 0
71
+ @code.lines[0..line - 1].each { |l|
72
+ offset += l.length
73
+ }
74
+ end
75
+ offset + col
76
+ end
77
+
78
+ def merge node
79
+ api_map.merge node
80
+ end
81
+
82
+ def tree_at(index)
83
+ arr = []
84
+ arr.push @node
85
+ inner_node_at(index, @node, arr)
86
+ arr
87
+ end
88
+
89
+ def node_at(index)
90
+ tree_at(index).first
91
+ end
92
+
93
+ def string_at?(index)
94
+ n = node_at(index)
95
+ n.kind_of?(AST::Node) and n.type == :str
96
+ end
97
+
98
+ def parent_node_from(index, *types)
99
+ arr = tree_at(index)
100
+ arr.each { |a|
101
+ if a.kind_of?(AST::Node) and (types.empty? or types.include?(a.type))
102
+ return a
103
+ end
104
+ }
105
+ @node
106
+ end
107
+
108
+ def namespace_at(index)
109
+ tree = tree_at(index)
110
+ return nil if tree.length == 0
111
+ node = parent_node_from(index, :module, :class)
112
+ slice = tree[(tree.index(node) || 0)..-1]
113
+ parts = []
114
+ slice.reverse.each { |n|
115
+ if n.type == :class or n.type == :module
116
+ parts.push unpack_name(n.children[0])
117
+ end
118
+ }
119
+ parts.join("::")
120
+ end
121
+
122
+ def phrase_at index
123
+ word = ''
124
+ cursor = index - 1
125
+ while cursor > -1
126
+ char = @code[cursor, 1]
127
+ break if char.nil? or char == ''
128
+ break unless char.match(/[\s;=\(\)\[\]\{\}]/).nil?
129
+ word = char + word
130
+ cursor -= 1
131
+ end
132
+ word
133
+ end
134
+
135
+ def word_at index
136
+ word = ''
137
+ cursor = index - 1
138
+ while cursor > -1
139
+ char = @code[cursor, 1]
140
+ break if char.nil? or char == ''
141
+ break unless char.match(/[a-z0-9_]/i)
142
+ word = char + word
143
+ cursor -= 1
144
+ end
145
+ word
146
+ end
147
+
148
+ def get_instance_variables_at(index)
149
+ node = parent_node_from(index, :def, :defs, :class, :module)
150
+ ns = namespace_at(index) || ''
151
+ @api_map.get_instance_variables(ns, (node.type == :def ? :instance : :class))
152
+ end
153
+
154
+ def suggest_at index, filtered: true, with_snippets: false
155
+ return [] if string_at?(index)
156
+ result = []
157
+ phrase = phrase_at(index)
158
+ if phrase.start_with?('@')
159
+ if phrase.include?('.')
160
+ result = []
161
+ # TODO: Temporarily assuming one period
162
+ var = phrase[0..phrase.index('.')-1]
163
+ ns = namespace_at(index)
164
+ obj = @api_map.infer_instance_variable(var, ns)
165
+ result = @api_map.get_instance_methods(obj) unless obj.nil?
166
+ else
167
+ result = get_instance_variables_at(index)
168
+ end
169
+ elsif phrase.start_with?('$')
170
+ result += @api_map.get_global_variables
171
+ elsif phrase.start_with?(':') and !phrase.start_with?('::')
172
+ # TODO: It's a symbol. Nothing to do for now.
173
+ return []
174
+ elsif phrase.include?('::')
175
+ parts = phrase.split('::', -1)
176
+ ns = parts[0..-2].join('::')
177
+ if parts.last.include?('.')
178
+ ns = parts[0..-2].join('::') + '::' + parts.last[0..parts.last.index('.')-1]
179
+ result = @api_map.get_methods(ns)
180
+ else
181
+ result = @api_map.namespaces_in(ns)
182
+ end
183
+ elsif phrase.include?('.')
184
+ # It's a method call
185
+ # TODO: For now we're assuming only one period. That's obviously a bad assumption.
186
+ #base = phrase[0..phrase.index('.')-1]
187
+ #ns_here = namespace_at(index)
188
+ #result = @api_map.get_methods(base, ns_here)
189
+ #scope = parent_node_from(index, :class, :module, :def, :defs) || @node
190
+ #var = find_local_variable_node(base, scope)
191
+ #unless var.nil?
192
+ # obj = infer(var.children[1])
193
+ # result = @api_map.get_instance_methods(obj) unless obj.nil?
194
+ #end
195
+
196
+ # TODO: Alternate version that resolves signature
197
+ result = resolve_signature_at index
198
+ else
199
+ current_namespace = namespace_at(index)
200
+ parts = current_namespace.to_s.split('::')
201
+ result += get_snippets_at(index) if with_snippets
202
+ result += get_local_variables_and_methods_at(index)
203
+ result += ApiMap.get_keywords(without_snippets: with_snippets)
204
+ while parts.length > 0
205
+ ns = parts.join('::')
206
+ result += @api_map.namespaces_in(ns)
207
+ parts.pop
208
+ end
209
+ result += @api_map.namespaces_in('')
210
+ end
211
+ result = reduce_starting_with(result, word_at(index)) if filtered
212
+ result
213
+ end
214
+
215
+ def resolve_signature_at index
216
+ signature = get_signature_at(index)
217
+ ns_here = namespace_at(index)
218
+ parts = signature.split('.')
219
+ first = parts.shift
220
+ scope = parent_node_from(index, :class, :module, :def, :defs) || @node
221
+ var = find_local_variable_node(first, scope)
222
+ if var.nil?
223
+ if parts.length == 0
224
+ return @api_map.get_methods(first, ns_here)
225
+ end
226
+ obj = get_method_return_value first, ns_here, parts.shift
227
+ while parts.length > 0
228
+ obj = get_instance_method_return_value obj, ns_here, parts.shift
229
+ end
230
+ return @api_map.get_instance_methods(obj) unless obj.nil?
231
+ else
232
+ obj = infer(var.children[1])
233
+ while parts.length > 0
234
+ obj = get_instance_method_return_value obj, ns_here, parts.shift
235
+ end
236
+ return @api_map.get_instance_methods(obj) unless obj.nil?
237
+ end
238
+ end
239
+
240
+ def get_method_return_value namespace, root, method
241
+ meths = @api_map.get_methods(namespace, root).delete_if{ |m| m.label != method }
242
+ meths.each { |m|
243
+ unless m.documentation.nil?
244
+ match = m.documentation.all.match(/@return \[([a-z0-9:_]*)/i)
245
+ klass = match[1]
246
+ return klass unless klass.nil?
247
+ end
248
+ }
249
+ 'Object'
250
+ end
251
+
252
+ def get_instance_method_return_value namespace, root, method
253
+ meths = @api_map.get_instance_methods(namespace, root).delete_if{ |m| m.label != method }
254
+ meths.each { |m|
255
+ unless m.documentation.nil?
256
+ match = m.documentation.all.match(/@return \[([a-z0-9:_]*)/i)
257
+ return match[1] unless match.nil?
258
+ end
259
+ }
260
+ 'Object'
261
+ end
262
+
263
+ def get_signature_at index
264
+ #node = node_at(index - 1)
265
+ #return '' unless node.type == :send
266
+ #parts = []
267
+ #build_signature(node, parts)
268
+ #return parts.join('.')
269
+
270
+ # TODO: Alternate rough version
271
+ brackets = 0
272
+ squares = 0
273
+ parens = 0
274
+ signature = ''
275
+ index -=1
276
+ while index > 0
277
+ break if brackets > 0 or parens > 0 or squares > 0
278
+ char = @code[index, 1]
279
+ if char == ')'
280
+ parens -=1
281
+ elsif char == ']'
282
+ squares -=1
283
+ elsif char == '}'
284
+ brackets -= 1
285
+ elsif char == '('
286
+ parens += 1
287
+ elsif char == '{'
288
+ brackets += 1
289
+ elsif char == '['
290
+ squares += 1
291
+ end
292
+ if brackets == 0 and parens == 0 and squares == 0
293
+ break if ['"', "'", ',', ' ', "\t", "\n"].include?(char)
294
+ signature = char + signature if char.match(/[a-z0-9:\._]/i)
295
+ end
296
+ index -= 1
297
+ end
298
+ signature
299
+ end
300
+
301
+ def build_signature(node, parts)
302
+ if node.kind_of?(AST::Node)
303
+ if node.type == :send
304
+ parts.unshift node.children[1].to_s
305
+ elsif node.type == :const
306
+ parts.unshift unpack_name(node)
307
+ end
308
+ build_signature(node.children[0], parts)
309
+ end
310
+ end
311
+
312
+ def get_snippets_at(index)
313
+ result = []
314
+ Snippets.definitions.each_pair { |name, detail|
315
+ matched = false
316
+ prefix = detail['prefix']
317
+ while prefix.length > 0
318
+ if @code[index-prefix.length, prefix.length] == prefix
319
+ matched = true
320
+ break
321
+ end
322
+ prefix = prefix[0..-2]
323
+ end
324
+ if matched
325
+ result.push Suggestion.new(detail['prefix'], kind: Suggestion::KEYWORD, detail: name, insert: detail['body'].join("\r\n"))
326
+ end
327
+ }
328
+ result
329
+ end
330
+
331
+ def get_local_variables_and_methods_at(index)
332
+ result = []
333
+ local = parent_node_from(index, :class, :module, :def, :defs) || @node
334
+ result += get_local_variables_from(local)
335
+ scope = namespace_at(index) || @node
336
+ if local.type == :def
337
+ result += @api_map.get_instance_methods(scope)
338
+ else
339
+ result += @api_map.get_methods(scope)
340
+ end
341
+ result += @api_map.get_methods('Kernel')
342
+ result
343
+ end
344
+
345
+ private
346
+
347
+ def reduce_starting_with(suggestions, word)
348
+ suggestions.reject { |s|
349
+ !s.label.start_with?(word)
350
+ }
351
+ end
352
+
353
+ def get_local_variables_from(node)
354
+ node ||= @node
355
+ arr = []
356
+ node.children.each { |c|
357
+ if c.kind_of?(AST::Node)
358
+ if c.type == :lvasgn
359
+ arr.push Suggestion.new(c.children[0], kind: Suggestion::VARIABLE)
360
+ else
361
+ arr += get_local_variables_from(c) unless [:class, :module, :def, :defs].include?(c.type)
362
+ end
363
+ end
364
+ }
365
+ arr
366
+ end
367
+
368
+ def inner_node_at(index, node, arr)
369
+ node.children.each { |c|
370
+ if c.kind_of?(AST::Node)
371
+ unless c.loc.expression.nil?
372
+ if index >= c.loc.expression.begin_pos
373
+ if c.respond_to?(:end)
374
+ if index < c.end.end_pos
375
+ arr.unshift c
376
+ end
377
+ elsif index < c.loc.expression.end_pos
378
+ arr.unshift c
379
+ end
380
+ end
381
+ end
382
+ inner_node_at(index, c, arr)
383
+ end
384
+ }
385
+ end
386
+
387
+ def find_local_variable_node name, scope
388
+ scope.children.each { |c|
389
+ if c.kind_of?(AST::Node)
390
+ if c.type == :lvasgn and c.children[0].to_s == name
391
+ return c
392
+ else
393
+ unless [:class, :module, :def, :defs].include?(c.type)
394
+ sub = find_local_variable_node(name, c)
395
+ return sub unless sub.nil?
396
+ end
397
+ end
398
+ end
399
+ }
400
+ nil
401
+ end
402
+
403
+ end
404
+ end
@@ -0,0 +1,265 @@
1
+ # Solargraph Ruby Parser
2
+ # Copyright (c) 2015 by Fred Snyder for Castwide Technologies LLC
3
+ #
4
+ # Solargraph::Parser builds a code representation of existing Ruby interfaces
5
+ # for use in the Solargraph IDE.
6
+ #
7
+ # Example use:
8
+ #
9
+ # parser = Solargraph::Parser.new
10
+ # parser.parse #=> String with the entire Ruby interface
11
+ # parser.parse("Fixnum") #=> String with the Fixnum interface
12
+ #require 'yard'
13
+ #require 'yard/registry'
14
+
15
+ module Solargraph
16
+ class LiveParser
17
+ def get_yard_return(path)
18
+ objects = []
19
+ yardocs = ['yard/2.2.0/.yardoc', 'ruby/yard/2.2.0/.yardoc-stdlib']
20
+ yardocs.each { |y|
21
+ YARD::Registry.load!(y)
22
+ o = YARD::Registry.at(path)
23
+ if !o.nil?
24
+ objects.push o
25
+ end
26
+ }
27
+ result = nil
28
+ objects.each { |x|
29
+ meth = x
30
+ if !meth.tag(:return) and meth.tag(:overload) and meth.tag(:overload).tag(:return)
31
+ meth = meth.tag(:overload)
32
+ end
33
+ meth.tags(:return).each { |r|
34
+ result = "#{r.types[0]}"
35
+ break
36
+ }
37
+ break if !result.nil?
38
+ }
39
+ result
40
+ end
41
+ def initialize
42
+
43
+ end
44
+ def parse namespace = nil
45
+ #puts "Namespace: #{namespace}"
46
+ @parsed = []
47
+ code = ""
48
+ fqns = namespace
49
+ if fqns.nil?
50
+ #code += parse("BasicObject")
51
+ #code += parse("Object")
52
+ #code += parse("Kernel")
53
+ code += parse("Module")
54
+ return code
55
+ end
56
+ mod = eval("#{fqns}")
57
+ if !mod.nil?
58
+ if mod.instance_of?(Class)
59
+ #puts "Parsing class #{mod} to #{fqns}"
60
+ code += parse_class mod, fqns
61
+ elsif mod.instance_of?(Module)
62
+ #puts "Parsing module #{mod} to #{fqns}"
63
+ code += parse_module mod, fqns
64
+ else
65
+ #raise "I don't know what a #{fqns} is."
66
+ code += "#{fqns} = nil\n"
67
+ end
68
+ else
69
+ #puts "NIL!"
70
+ end
71
+ code
72
+ end
73
+ def self.parse n
74
+ LiveParser.new.parse(n)
75
+ end
76
+ private
77
+ def parse_class cls, rel_name
78
+ return "" if @parsed.include?(cls)
79
+ @parsed.push cls
80
+ code = ""
81
+ #code += "class #{rel_name}"
82
+ code += "class #{cls}"
83
+ if !cls.superclass.nil? && cls.superclass != cls
84
+ code += " < #{cls.superclass}"
85
+ end
86
+ code += "\n"
87
+ code += parse_class_internals(cls)
88
+ code += "end\n"
89
+ cls.constants().each { |c|
90
+ #obj = cls.class_eval(c.to_s)
91
+ begin
92
+ obj = cls.const_get(c)
93
+ if obj.kind_of?(Class)
94
+ code += parse_class(obj, c)
95
+ elsif obj.kind_of?(Module)
96
+ code += parse_module(obj, c)
97
+ else
98
+ #code += subparse(obj)
99
+ end
100
+ #rescue NameError => e
101
+ # #puts "NOPE! NOT #{c}"
102
+ #end
103
+ rescue Exception => e
104
+ # TODO: Ignoring all exceptions for now
105
+ end
106
+ }
107
+ code
108
+ end
109
+ def parse_module mod, rel_name
110
+ return "" if @parsed.include?(mod) or mod == Solargraph
111
+ @parsed.push mod
112
+ code = ""
113
+ #if (mod.to_s != "Kernel")
114
+ code = "module #{mod}\n"
115
+ #end
116
+ code += parse_module_internals(mod)
117
+ #if (mod.to_s != "Kernel")
118
+ code += "end\n"
119
+ #end
120
+ mod.constants().each { |c|
121
+ #obj = mod.class_eval(c.to_s)
122
+ begin
123
+ obj = mod.const_get(c)
124
+ rescue LoadError => e
125
+ code += "# @todo Failed to load #{c} from #{mod}\n"
126
+ end
127
+ if obj.kind_of?(Class)
128
+ code += parse_class(obj, c)
129
+ elsif obj.kind_of?(Module)
130
+ code += parse_module(obj, c)
131
+ else
132
+ #code += subparse(obj)
133
+ end
134
+ }
135
+ code
136
+ end
137
+ def parse_class_internals obj
138
+ code = ""
139
+ obj.included_modules.each { |inc|
140
+ #if (inc.to_s != "Kernel")
141
+ code += "include #{inc}\n"
142
+ #end
143
+ }
144
+ obj.public_methods(false).each { |m|
145
+ if !can_ignore?(obj, m)
146
+ args = build_args obj.method(m)
147
+ #ret = get_yard_return "#{obj}::#{m}"
148
+ #if !ret.nil?
149
+ # code += "# @return [#{ret}]\n"
150
+ #end
151
+ code += "def self.#{m}#{args};end\n"
152
+ end
153
+ }
154
+ alloc = obj
155
+ obj.singleton_methods(false).each { |m|
156
+ if !can_ignore?(obj, m)
157
+ args = build_args obj.method(m)
158
+ #ret = get_yard_return "#{obj}::#{m}"
159
+ #if !ret.nil?
160
+ # code += "# @return [#{ret}]\n"
161
+ #end
162
+ code += "def self.#{m}#{args};end\n"
163
+ end
164
+ }
165
+ obj.public_instance_methods(false).each { |m|
166
+ if !can_ignore?(obj, m)
167
+ begin
168
+ args = build_args obj.public_instance_method(m)
169
+ rescue TypeError => e
170
+ args = ""
171
+ end
172
+ #ret = get_yard_return "#{obj}##{m}"
173
+ #if !ret.nil?
174
+ # code += "# @return [#{ret}]\n"
175
+ #end
176
+ code += "def #{m}#{args};end\n"
177
+ end
178
+ }
179
+ code
180
+ end
181
+ def parse_module_internals obj
182
+ code = ""
183
+ obj.included_modules.each { |inc|
184
+ #if (inc.to_s != "Kernel")
185
+ code += "include #{inc}\n"
186
+ #end
187
+ }
188
+ obj.public_methods(false).each { |m|
189
+ if obj == Kernel #and obj.singleton_methods.include?(m)
190
+ next
191
+ end
192
+ if !can_ignore?(obj, m)
193
+ args = build_args obj.method(m)
194
+ #ret = get_yard_return "#{obj}##{m}"
195
+ #if !ret.nil?
196
+ # code += "# @return [#{ret}]\n"
197
+ #end
198
+ code += "def #{m}#{args};end\n"
199
+ end
200
+ }
201
+ obj.singleton_methods(false).each { |m|
202
+ if !can_ignore?(obj, m)
203
+ args = build_args obj.method(m)
204
+ #ret = get_yard_return "#{obj}::#{m}"
205
+ #if !ret.nil?
206
+ # code += "# @return [#{ret}]\n"
207
+ #end
208
+ code += "def self.#{m}#{args};end\n"
209
+ end
210
+ }
211
+ #obj.public_instance_methods(false).each { |m|
212
+ obj.public_instance_methods(false).each { |m|
213
+ #if !can_ignore?(obj, m)
214
+ args = build_args obj.public_instance_method(m)
215
+ #ret = get_yard_return "#{obj}##{m}"
216
+ #if !ret.nil?
217
+ # code += "# @return [#{ret}]\n"
218
+ #end
219
+ code += "def #{m}#{args};end\n"
220
+ #end
221
+ }
222
+ code
223
+ end
224
+ def can_ignore?(obj, sym)
225
+ #return false
226
+ basics = [Kernel, Module, Object, BasicObject]
227
+ return false if basics.include?(obj)
228
+ result = false
229
+ basics.each { |b|
230
+ if b.respond_to?(sym)
231
+ result = true
232
+ break
233
+ end
234
+ }
235
+ return result
236
+ end
237
+ def build_args method
238
+ args = ""
239
+ if (method.arity == -1)
240
+ args = "(*args)"
241
+ else
242
+ arr = []
243
+ num = 0
244
+ method.parameters.each { |p|
245
+ n = p[1]
246
+ if n.to_s == ""
247
+ n = "arg#{num}"
248
+ end
249
+ if p[0] == :req
250
+ arr.push "#{n}"
251
+ elsif p[0] == :opt
252
+ arr.push "#{n} = nil"
253
+ elsif p[0] == :rest
254
+ arr.push "*#{n}"
255
+ elsif p[0] == :block
256
+ arr.push "&#{n}"
257
+ end
258
+ num += 1
259
+ }
260
+ args = "(" + arr.join(", ") + ")"
261
+ end
262
+ args
263
+ end
264
+ end
265
+ end