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)


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

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
  live_map
  refresh
end

Instance Attribute Details

#requiredArray<String> (readonly)

Returns:

  • (Array<String>)


35
36
37
# File 'lib/solargraph/api_map.rb', line 35

def required
  @required
end

#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:



116
117
118
119
120
# File 'lib/solargraph/api_map.rb', line 116

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



97
98
99
# File 'lib/solargraph/api_map.rb', line 97

def append_source code, filename
  virtualize filename, code
end

#configObject



66
67
68
# File 'lib/solargraph/api_map.rb', line 66

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

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

Returns:

  • (String)


159
160
161
162
163
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
# File 'lib/solargraph/api_map.rb', line 159

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
  yard_map.find_fully_qualified_namespace(name, root)
end

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

Returns:



220
221
222
223
# File 'lib/solargraph/api_map.rb', line 220

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

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

Returns:



226
227
228
229
230
231
232
233
234
235
236
# File 'lib/solargraph/api_map.rb', line 226

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)


109
110
111
112
113
# File 'lib/solargraph/api_map.rb', line 109

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:



140
141
142
143
# File 'lib/solargraph/api_map.rb', line 140

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:



146
147
148
149
150
151
152
153
154
155
156
# File 'lib/solargraph/api_map.rb', line 146

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(namespace, root)
end

#get_filename_for(node) ⇒ String

Returns:

  • (String)


245
246
247
248
249
250
# File 'lib/solargraph/api_map.rb', line 245

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:



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

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



438
439
440
441
442
443
444
445
446
447
448
# File 'lib/solargraph/api_map.rb', line 438

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:



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/solargraph/api_map.rb', line 410

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(namespace, root)
    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_instance_methods(namespace, root, 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)
  end
  meths
end

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



201
202
203
204
# File 'lib/solargraph/api_map.rb', line 201

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:



207
208
209
210
211
212
213
214
215
216
217
# File 'lib/solargraph/api_map.rb', line 207

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:



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/solargraph/api_map.rb', line 371

def get_methods(namespace, root = '', visibility: [:public])
  refresh
  namespace = clean_namespace_string(namespace)
  fqns = find_fully_qualified_namespace(namespace, root)
  meths = []
  meths.concat inner_get_methods(namespace, root, []) #unless has_yardoc?
  yard_meths = yard_map.get_methods(namespace, root, visibility: visibility)
  if yard_meths.any?
    meths.concat yard_meths
  else
    type = get_namespace_type(namespace, root)
    if type == :class
      meths.concat yard_map.get_instance_methods('Class')
    elsif type == :module
      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
  strings = meths.map(&:to_s)
  live_map.get_methods(namespace, root, 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)'))
  end
meths
end

#get_namespace_nodes(fqns) ⇒ Object



194
195
196
197
198
# File 'lib/solargraph/api_map.rb', line 194

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

#get_namespace_type(namespace, root = '') ⇒ Object



357
358
359
360
361
362
363
364
365
# File 'lib/solargraph/api_map.rb', line 357

def get_namespace_type namespace, root = ''
  type = nil
  fqns = find_fully_qualified_namespace(namespace, root)
  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
  type
end

#get_path_suggestions(path) ⇒ Object



460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# File 'lib/solargraph/api_map.rb', line 460

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_instance_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:



239
240
241
242
# File 'lib/solargraph/api_map.rb', line 239

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

#infer_assignment_node_type(node, namespace) ⇒ String

Returns:

  • (String)


285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/solargraph/api_map.rb', line 285

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)


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

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)


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

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)


314
315
316
317
318
319
320
321
322
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
# File 'lib/solargraph/api_map.rb', line 314

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:



80
81
82
# File 'lib/solargraph/api_map.rb', line 80

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

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

Returns:

  • (Boolean)


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

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

#namespacesObject



122
123
124
125
# File 'lib/solargraph/api_map.rb', line 122

def namespaces
  refresh
  namespace_map.keys
end

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



131
132
133
134
135
136
137
# File 'lib/solargraph/api_map.rb', line 131

def namespaces_in name, root = ''
  refresh
  result = []
  result += inner_namespaces_in(name, root, [])
  result += yard_map.get_constants name, root
  result
end

#refresh(force = false) ⇒ Object



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

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

#sourcesObject



456
457
458
# File 'lib/solargraph/api_map.rb', line 456

def sources
  @sources.values
end

#update(filename) ⇒ Object



450
451
452
453
454
# File 'lib/solargraph/api_map.rb', line 450

def update filename
  @@source_cache[filename] ||= Source.load(filename)
  #cache.clear
  @stale = true
end

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



85
86
87
88
89
90
91
92
93
94
# File 'lib/solargraph/api_map.rb', line 85

def virtualize filename, code, 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(filename, code, cursor)
  process_virtual
  @virtual_source
end

#yard_mapSolargraph::YardMap

Returns:



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

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