Class: Solargraph::ApiMap

Inherits:
Object
  • Object
show all
Includes:
NodeMethods, YardMethods
Defined in:
lib/solargraph/api_map.rb,
lib/solargraph/api_map/cache.rb,
lib/solargraph/api_map/config.rb,
lib/solargraph/api_map/source.rb

Defined Under Namespace

Classes: Cache, Config, Source

Constant Summary collapse

KEYWORDS =
[
  '__ENCODING__', '__LINE__', '__FILE__', 'BEGIN', 'END', 'alias', 'and',
  'begin', 'break', 'case', 'class', 'def', 'defined?', 'do', 'else',
  'elsif', 'end', 'ensure', 'false', 'for', 'if', 'in', 'module', 'next',
  'nil', 'not', 'or', 'redo', 'rescue', 'retry', 'return', 'self', 'super',
  'then', 'true', 'undef', 'unless', 'until', 'when', 'while', 'yield'
].freeze
METHODS_RETURNING_SELF =
[
  'clone', 'dup', 'freeze', 'taint', 'untaint'
].freeze
@@source_cache =
{}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from YardMethods

#yard_files, #yard_options

Methods included from NodeMethods

#const_from, #infer_literal_node_type, #pack_name, #resolve_node_signature, #unpack_name

Constructor Details

#initialize(workspace = nil) ⇒ ApiMap

Returns a new instance of ApiMap.

Parameters:

  • workspace (String) (defaults to: nil)


35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/solargraph/api_map.rb', line 35

def initialize workspace = nil
  @workspace = workspace.gsub(/\\/, '/') unless workspace.nil?
  clear
  # @todo Instead of requiring extensions to be listed in the config,
  # we're experimenting with automatically loading them.
  #self.config.extensions.each do |ext|
  #  require ext
  #end
  require_extensions
  @workspace_files = []
  unless @workspace.nil?
    @workspace_files.concat (self.config.included - self.config.excluded)
    @workspace_files.each do |wf|
      begin
        @@source_cache[wf] ||= Source.load(wf)
      rescue
        STDERR.puts "Failed to load #{wf}"
      end
    end
  end
  @sources = {}
  @virtual_source = nil
  @virtual_filename = nil
  @stale = true
  refresh
  yard_map
end

Instance Attribute Details

#workspaceString (readonly)

The root directory of the project. The ApiMap will search here for additional files to parse and analyze.

Returns:

  • (String)


32
33
34
# File 'lib/solargraph/api_map.rb', line 32

def workspace
  @workspace
end

Class Method Details

.get_keywordsArray<Solargraph::Suggestion>

Returns:



119
120
121
122
123
# File 'lib/solargraph/api_map.rb', line 119

def self.get_keywords
  @keyword_suggestions ||= KEYWORDS.map{ |s|
    Suggestion.new(s.to_s, kind: Suggestion::KEYWORD, detail: 'Keyword')
  }.freeze
end

Instance Method Details

#append_source(code, filename) ⇒ Solargraph::ApiMap::Source



100
101
102
# File 'lib/solargraph/api_map.rb', line 100

def append_source code, filename
  virtualize code, filename
end

#configObject



63
64
65
# File 'lib/solargraph/api_map.rb', line 63

def config
  @config ||= ApiMap::Config.new(@workspace)
end

#find_fully_qualified_namespace(name, root = '', skip = []) ⇒ String

Returns:

  • (String)


164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/solargraph/api_map.rb', line 164

def find_fully_qualified_namespace name, root = '', skip = []
  refresh
  return nil if skip.include?(root)
  skip.push root
  if name == ''
    if root == ''
      return ''
    else
      return find_fully_qualified_namespace(root, '', skip)
    end
  else
    if (root == '')
      return name unless namespace_map[name].nil?
      get_include_strings_from(*file_nodes).each { |i|
        reroot = "#{root == '' ? '' : root + '::'}#{i}"
        recname = find_fully_qualified_namespace name.to_s, reroot, skip
        return recname unless recname.nil?
      }
    else
      roots = root.to_s.split('::')
      while roots.length > 0
        fqns = roots.join('::') + '::' + name
        return fqns unless namespace_map[fqns].nil?
        roots.pop
      end
      return name unless namespace_map[name].nil?
      get_include_strings_from(*file_nodes).each { |i|
        recname = find_fully_qualified_namespace name, i, skip
        return recname unless recname.nil?
      }
    end
  end
  result = yard_map.find_fully_qualified_namespace(name, root)
  if result.nil?
    result = live_map.get_fqns(name, root)
  end
  result
end

#get_class_variable_pins(namespace) ⇒ Array<Solargraph::Pin::ClassVariable>

Returns:



229
230
231
232
# File 'lib/solargraph/api_map.rb', line 229

def get_class_variable_pins(namespace)
  refresh
  @cvar_pins[namespace] || []
end

#get_class_variables(namespace) ⇒ Array<Solargraph::Suggestion>

Returns:



235
236
237
238
239
240
241
242
243
244
245
# File 'lib/solargraph/api_map.rb', line 235

def get_class_variables(namespace)
  refresh
  result = []
  ip = @cvar_pins[namespace]
  unless ip.nil?
    ip.each do |pin|
      result.push pin_to_suggestion(pin)
    end
  end
  result
end

#get_comment_for(node) ⇒ YARD::Docstring

Get the docstring associated with a node.

Parameters:

  • node (AST::Node)

Returns:

  • (YARD::Docstring)


112
113
114
115
116
# File 'lib/solargraph/api_map.rb', line 112

def get_comment_for node
  filename = get_filename_for(node)
  return nil if @sources[filename].nil?
  @sources[filename].docstring_for(node)
end

#get_constant_pins(namespace, root) ⇒ Array<Solargraph::Pin::Constant>

Returns:



145
146
147
148
# File 'lib/solargraph/api_map.rb', line 145

def get_constant_pins namespace, root
  fqns = find_fully_qualified_namespace(namespace, root)
  @const_pins[fqns] || []
end

#get_constants(namespace, root) ⇒ Array<Solargraph::Suggestion>

Returns:



151
152
153
154
155
156
157
158
159
160
161
# File 'lib/solargraph/api_map.rb', line 151

def get_constants namespace, root
  result = []
  fqns = find_fully_qualified_namespace(namespace, root)
  cp = @const_pins[fqns]
  unless cp.nil?
    cp.each do |pin|
      result.push pin_to_suggestion(pin)
    end
  end
  result.concat yard_map.get_constants(fqns)
end

#get_filename_for(node) ⇒ String

Returns:

  • (String)


254
255
256
257
258
259
# File 'lib/solargraph/api_map.rb', line 254

def get_filename_for(node)
  @sources.each do |filename, source|
    return source.filename if source.include?(node)
  end
  nil
end

#get_global_variablesArray<Solargraph::Suggestion>

Returns:



283
284
285
286
287
288
289
290
291
# File 'lib/solargraph/api_map.rb', line 283

def get_global_variables
  result = []
  @sources.values.each do |s|
    s.global_variable_pins.each do |p|
      result.push pin_to_suggestion(p)
    end
  end
  result
end

#get_include_strings_from(*nodes) ⇒ Object



456
457
458
459
460
461
462
463
464
465
466
# File 'lib/solargraph/api_map.rb', line 456

def get_include_strings_from *nodes
  arr = []
  nodes.each { |node|
    next unless node.kind_of?(AST::Node)
    arr.push unpack_name(node.children[2]) if (node.type == :send and node.children[1] == :include)
    node.children.each { |n|
      arr += get_include_strings_from(n) if n.kind_of?(AST::Node) and n.type != :class and n.type != :module
    }
  }
  arr
end

#get_instance_methods(namespace, root = '', visibility: [:public]) ⇒ Array<Solargraph::Suggestion>

Get an array of instance methods that are available in the specified namespace.

Returns:



428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/solargraph/api_map.rb', line 428

def get_instance_methods(namespace, root = '', visibility: [:public])
  refresh
  namespace = clean_namespace_string(namespace)
  if namespace.end_with?('#class')
    return get_methods(namespace.split('#').first, root, visibility: visibility)
  end
  meths = []
  meths += inner_get_instance_methods(namespace, root, [], visibility) #unless has_yardoc?
  fqns = find_fully_qualified_namespace(namespace, root)
  yard_meths = yard_map.get_instance_methods(fqns, '', visibility: visibility)
  if yard_meths.any?
    meths.concat yard_meths
  else
    type = get_namespace_type(fqns)
    if type == :class
      meths += yard_map.get_instance_methods('Object')
    elsif type == :module
      meths += yard_map.get_instance_methods('Module')
    end
  end
  strings = meths.map(&:to_s)
  live_map.get_methods(namespace, root, 'instance', visibility.include?(:private)).each do |m|
    next if strings.include?(m) or !m.match(/^[a-z]/i)
    meths.push Suggestion.new(m, kind: Suggestion::METHOD, docstring: YARD::Docstring.new('(defined at runtime)'), path: "#{fqns}##{m}")
  end
  meths
end

#get_instance_variable_pins(namespace, scope = :instance) ⇒ Array<Solargraph::Pin::InstanceVariable>



210
211
212
213
# File 'lib/solargraph/api_map.rb', line 210

def get_instance_variable_pins(namespace, scope = :instance)
  refresh
  (@ivar_pins[namespace] || []).select{ |pin| pin.scope == scope }
end

#get_instance_variables(namespace, scope = :instance) ⇒ Array<Solargraph::Suggestion>

Returns:



216
217
218
219
220
221
222
223
224
225
226
# File 'lib/solargraph/api_map.rb', line 216

def get_instance_variables(namespace, scope = :instance)
  refresh
  result = []
  ip = @ivar_pins[namespace]
  unless ip.nil?
    ip.select{ |pin| pin.scope == scope }.each do |pin|
      result.push pin_to_suggestion(pin)
    end
  end
  result
end

#get_methods(namespace, root = '', visibility: [:public]) ⇒ Array<Solargraph::Suggestion>

Get an array of singleton methods that are available in the specified namespace.

Returns:



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/solargraph/api_map.rb', line 383

def get_methods(namespace, root = '', visibility: [:public])
  refresh
  namespace = clean_namespace_string(namespace)
  fqns = find_fully_qualified_namespace(namespace, root)
  meths = []
  skip = []
  meths.concat inner_get_methods(namespace, root, skip)
  yard_meths = yard_map.get_methods(fqns, '', visibility: visibility)
  if yard_meths.any?
    meths.concat yard_meths
  else
    type = get_namespace_type(fqns)
    if type == :class
      meths.concat yard_map.get_instance_methods('Class')
    else
      meths.concat yard_map.get_methods('Module')
    end
  end
  news = meths.select{|s| s.label == 'new'}
  unless news.empty?
    if @method_pins[fqns]
      inits = @method_pins[fqns].select{|p| p.name == 'initialize'}
      meths -= news unless inits.empty?
      inits.each do |pin|
        meths.push Suggestion.new('new', kind: pin.kind, docstring: pin.docstring, detail: pin.namespace, arguments: pin.parameters, path: pin.path)
      end
    end
  end
  if namespace == '' and root == ''
    config.domains.each do |d|
      meths.concat get_instance_methods(d)
    end
  end
  strings = meths.map(&:to_s)
  live_map.get_methods(fqns, '', 'class', visibility.include?(:private)).each do |m|
    next if strings.include?(m) or !m.match(/^[a-z]/i)
    meths.push Suggestion.new(m, kind: Suggestion::METHOD, docstring: YARD::Docstring.new('(defined at runtime)'), path: "#{fqns}.#{m}")
  end
  meths
end

#get_namespace_nodes(fqns) ⇒ Object



203
204
205
206
207
# File 'lib/solargraph/api_map.rb', line 203

def get_namespace_nodes(fqns)
  return file_nodes if fqns == '' or fqns.nil?
  refresh
  namespace_map[fqns] || []
end

#get_namespace_type(fqns) ⇒ Object



366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/solargraph/api_map.rb', line 366

def get_namespace_type fqns
  return nil if fqns.nil?
  type = nil
  nodes = get_namespace_nodes(fqns)
  unless nodes.nil? or nodes.empty? or !nodes[0].kind_of?(AST::Node)
    type = nodes[0].type if [:class, :module].include?(nodes[0].type)
  end
  if type.nil?
    type = yard_map.get_namespace_type(fqns)
  end
  type
end

#get_path_suggestions(path) ⇒ Object



480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# File 'lib/solargraph/api_map.rb', line 480

def get_path_suggestions path
  refresh
  result = []
  if path.include?('#')
    # It's an instance method
    parts = path.split('#')
    result = get_instance_methods(parts[0], '', visibility: [:public, :private, :protected]).select{|s| s.label == parts[1]}
  elsif path.include?('.')
    # It's a class method
    parts = path.split('.')
    result = get_methods(parts[0], '', visibility: [:public, :private, :protected]).select{|s| s.label == parts[1]}
  else
    # It's a class or module
    get_namespace_nodes(path).each do |node|
      # TODO This is way underimplemented
      result.push Suggestion.new(path, kind: Suggestion::CLASS)
    end
    result.concat yard_map.objects(path)
  end
  result
end

#get_symbolsArray<Solargraph::Pin::Symbol>

Returns:



248
249
250
251
# File 'lib/solargraph/api_map.rb', line 248

def get_symbols
  refresh
  @symbol_pins.uniq(&:label)
end

#infer_assignment_node_type(node, namespace) ⇒ String

Returns:

  • (String)


294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/solargraph/api_map.rb', line 294

def infer_assignment_node_type node, namespace
  type = cache.get_assignment_node_type(node, namespace)
  if type.nil?
    cmnt = get_comment_for(node)
    if cmnt.nil?
      name_i = (node.type == :casgn ? 1 : 0) 
      sig_i = (node.type == :casgn ? 2 : 1)
      type = infer_literal_node_type(node.children[sig_i])
      if type.nil?
        sig = resolve_node_signature(node.children[sig_i])
        # Avoid infinite loops from variable assignments that reference themselves
        return nil if node.children[name_i].to_s == sig.split('.').first
        type = infer_signature_type(sig, namespace)
      end
    else
      t = cmnt.tag(:type)
      if t.nil?
        sig = resolve_node_signature(node.children[1])
        type = infer_signature_type(sig, namespace)
      else
        type = t.types[0]
      end
    end
    cache.set_assignment_node_type(node, namespace, type)
  end
  type
end

#infer_class_variable(var, namespace) ⇒ String

Returns:

  • (String)


272
273
274
275
276
277
278
279
280
# File 'lib/solargraph/api_map.rb', line 272

def infer_class_variable(var, namespace)
  refresh
  fqns = find_fully_qualified_namespace(namespace)
  pins = @cvar_pins[fqns]
  return nil if pins.nil?
  pin = pins.select{|p| p.name == var}.first
  return nil if pin.nil?
  pin.return_type
end

#infer_instance_variable(var, namespace, scope) ⇒ String

Returns:

  • (String)


262
263
264
265
266
267
268
269
# File 'lib/solargraph/api_map.rb', line 262

def infer_instance_variable(var, namespace, scope)
  refresh
  pins = @ivar_pins[namespace]
  return nil if pins.nil?
  pin = pins.select{|p| p.name == var and p.scope == scope}.first
  return nil if pin.nil?
  pin.return_type
end

#infer_signature_type(signature, namespace, scope: :class) ⇒ String

Returns:

  • (String)


323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/solargraph/api_map.rb', line 323

def infer_signature_type signature, namespace, scope: :class
  if cache.has_signature_type?(signature, namespace, scope)
    return cache.get_signature_type(signature, namespace, scope)
  end
  return nil if signature.nil? or signature.empty?
  result = nil
  if namespace.end_with?('#class')
    result = infer_signature_type signature, namespace[0..-7], scope: (scope == :class ? :instance : :class)
  else
    parts = signature.split('.', 2)
    if parts[0].start_with?('@@')
      type = infer_class_variable(parts[0], namespace)
      if type.nil? or parts.empty?
        result = inner_infer_signature_type(parts[1], type, scope: :instance)
      else
        result = type
      end
    elsif parts[0].start_with?('@')
      type = infer_instance_variable(parts[0], namespace, scope)
      if type.nil? or parts.empty?
        result = inner_infer_signature_type(parts[1], type, scope: :instance)
      else
        result = type
      end
    else
      type = find_fully_qualified_namespace(parts[0], namespace)
      if type.nil?
        # It's a method call
        type = inner_infer_signature_type(parts[0], namespace, scope: scope)
        if parts[1].nil?
          result = type
        else
          result = inner_infer_signature_type(parts[1], type, scope: :instance)
        end
      else
        result = inner_infer_signature_type(parts[1], type, scope: :class)
      end
    end
  end
  cache.set_signature_type signature, namespace, scope, result
  result
end

#live_mapSolargraph::LiveMap

Returns:



82
83
84
# File 'lib/solargraph/api_map.rb', line 82

def live_map
  @live_map ||= Solargraph::LiveMap.new(self)
end

#namespace_exists?(name, root = '') ⇒ Boolean

Returns:

  • (Boolean)


130
131
132
# File 'lib/solargraph/api_map.rb', line 130

def namespace_exists? name, root = ''
  !find_fully_qualified_namespace(name, root).nil?
end

#namespacesObject



125
126
127
128
# File 'lib/solargraph/api_map.rb', line 125

def namespaces
  refresh
  namespace_map.keys
end

#namespaces_in(name, root = '') ⇒ Object



134
135
136
137
138
139
140
141
142
# File 'lib/solargraph/api_map.rb', line 134

def namespaces_in name, root = ''
  refresh
  result = []
  result += inner_namespaces_in(name, root, [])
  result += yard_map.get_constants name, root
  strings = result.map(&:to_s)
  result.concat live_map.get_constants(name, root)
  result
end

#refresh(force = false) ⇒ Object



104
105
106
# File 'lib/solargraph/api_map.rb', line 104

def refresh force = false
  process_maps if @stale or force
end

#requiredArray<String>

Returns:

  • (Array<String>)


68
69
70
# File 'lib/solargraph/api_map.rb', line 68

def required
  @required ||= []
end

#sourcesObject



476
477
478
# File 'lib/solargraph/api_map.rb', line 476

def sources
  @sources.values
end

#update(filename) ⇒ Object



468
469
470
471
472
473
474
# File 'lib/solargraph/api_map.rb', line 468

def update filename
  filename.gsub!(/\\/, '/')
  eliminate filename
  @@source_cache[filename] = Source.load(filename)
  rebuild_local_yardoc #if @workspace_files.include?(filename)
  @stale = true
end

#virtualize(code, filename = nil, cursor = nil) ⇒ Solargraph::ApiMap::Source



87
88
89
90
91
92
93
94
95
96
97
# File 'lib/solargraph/api_map.rb', line 87

def virtualize code, filename = nil, cursor = nil
  unless @virtual_source.nil? or @virtual_filename == filename or @workspace_files.include?(@virtual_filename)
    eliminate @virtual_filename
  end
  #refresh
  @virtual_filename = filename
  @virtual_source = Source.fix(code, filename, cursor)
  process_virtual
  #@stale = true
  @virtual_source
end

#yard_mapSolargraph::YardMap

Returns:



73
74
75
76
77
78
79
# File 'lib/solargraph/api_map.rb', line 73

def yard_map
  refresh
  if @yard_map.nil? || @yard_map.required != required
    @yard_map = Solargraph::YardMap.new(required: required, workspace: workspace)
  end
  @yard_map
end