solargraph 0.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.
@@ -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