Class: Solargraph::ApiMap

Inherits:
Object
  • Object
show all
Includes:
NodeMethods
Defined in:
lib/solargraph/api_map.rb

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'
]
MAPPABLE_METHODS =
[
  :include, :require, :autoload, :attr_reader, :attr_writer, :attr_accessor, :private, :public, :protected
]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from NodeMethods

#infer, #pack_name, #unpack_name

Constructor Details

#initialize(workspace = nil) ⇒ ApiMap

Returns a new instance of ApiMap.



24
25
26
27
28
29
30
31
32
33
# File 'lib/solargraph/api_map.rb', line 24

def initialize workspace = nil
  @workspace = workspace
  clear
  unless @workspace.nil?
    files = Dir[File.join @workspace, 'lib', '**', '*.rb'] + Dir[File.join @workspace, 'app', '**', '*.rb']
    files.each { |f|
      append_file f unless yardoc_has_file?(f)
    }
  end
end

Instance Attribute Details

#requiredObject (readonly)

Returns the value of attribute required.



22
23
24
# File 'lib/solargraph/api_map.rb', line 22

def required
  @required
end

#workspaceObject (readonly)

Returns the value of attribute workspace.



21
22
23
# File 'lib/solargraph/api_map.rb', line 21

def workspace
  @workspace
end

Class Method Details

.currentObject



382
383
384
385
386
387
388
# File 'lib/solargraph/api_map.rb', line 382

def self.current
  if @current.nil?
    @current = ApiMap.new
    @current.merge(Parser::CurrentRuby.parse(File.read("#{Solargraph::STUB_PATH}/ruby/2.3.0/core.rb")))
  end
  @current
end

.get_keywords(without_snippets: false) ⇒ Object



98
99
100
101
102
103
104
105
106
# File 'lib/solargraph/api_map.rb', line 98

def self.get_keywords without_snippets: false
  result = []
  keywords = KEYWORDS
  keywords -= Snippets.keywords if without_snippets
  keywords.each { |k|
    result.push Suggestion.new(k, kind: Suggestion::KEYWORD, detail: 'Keyword')
  }
  result
end

Instance Method Details

#append_file(filename) ⇒ Object



57
58
59
# File 'lib/solargraph/api_map.rb', line 57

def append_file filename
  append_source File.read(filename), filename
end

#append_node(node, comments, filename = nil) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
# File 'lib/solargraph/api_map.rb', line 66

def append_node node, comments, filename = nil
  @file_comments[filename] = associate_comments(node, comments)
  mapified = reduce(node, @file_comments[filename])
  root = AST::Node.new(:begin, [filename])
  mapified.children.each { |c|
    root = root.append c
  }
  @file_nodes[filename] = root
  @required.uniq!
  process_maps
end

#append_source(text, filename = nil) ⇒ Object



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

def append_source text, filename = nil
  node, comments = Parser::CurrentRuby.parse_with_comments(text)
  append_node(node, comments, filename)
end

#associate_comments(node, comments) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/solargraph/api_map.rb', line 78

def associate_comments node, comments
  comment_hash = Parser::Source::Comment.associate_locations(node, comments)
  yard_hash = {}
  comment_hash.each_pair { |k, v|
    ctxt = ''
    v.each { |l|
      ctxt += l.text.gsub(/^#/, '') + "\n"
    }
    parser = YARD::DocstringParser.new
    yard_hash[k] = parser.parse(ctxt).to_docstring
  }
  yard_hash
end

#clearObject



35
36
37
38
39
40
41
42
# File 'lib/solargraph/api_map.rb', line 35

def clear
  @file_nodes = {}
  @file_comments = {}
  @parent_stack = {}
  @namespace_map = {}
  @namespace_tree = {}
  @required = []
end

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



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/solargraph/api_map.rb', line 182

def find_fully_qualified_namespace name, root = '', skip = []
  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.values).each { |i|
        reroot = "#{root == '' ? '' : root + '::'}#{i}"
        recname = find_fully_qualified_namespace name, 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.values).each { |i|
        recname = find_fully_qualified_namespace name, i, skip
        return recname unless recname.nil?
      }
    end
  end
  nil
end

#find_instance_variable_assignment(var, node, scope) ⇒ Object



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/solargraph/api_map.rb', line 277

def find_instance_variable_assignment(var, node, scope)
  node.children.each { |c|
    if c.kind_of?(AST::Node)
      is_inst = !find_parent(c, :def).nil?
      if c.type == :ivasgn and ( (scope == :instance and is_inst) or (scope != :instance and !is_inst) )
        if c.children[0].to_s == var
          return c
        end
      else
        inner = find_instance_variable_assignment(var, c, scope)
        return inner unless inner.nil?
      end
    end
  }
  nil
end

#find_parent(node, *types) ⇒ Object



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

def find_parent(node, *types)
  parents = @parent_stack[node]
  parents.each { |p|
    return p if types.include?(p.type)
  }
  nil
end

#get_comment_for(node) ⇒ Object



92
93
94
95
96
# File 'lib/solargraph/api_map.rb', line 92

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

#get_filename_for(node) ⇒ Object



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

def get_filename_for(node)
  root = get_root_for(node)
  root.nil? ? nil : root.children[0]
end

#get_global_variablesObject



294
295
296
297
# File 'lib/solargraph/api_map.rb', line 294

def get_global_variables
  # TODO: Get them
  []
end

#get_include_strings_from(*nodes) ⇒ Object



390
391
392
393
394
395
396
397
398
399
400
# File 'lib/solargraph/api_map.rb', line 390

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]) ⇒ Object



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/solargraph/api_map.rb', line 345

def get_instance_methods(namespace, root = '', visibility: [:public])
  meths = []
  meths += inner_get_instance_methods(namespace, root, []) #unless has_yardoc?
  yard = YardMap.new(required: @required, workspace: @workspace)
  yard_meths = yard.get_instance_methods(namespace, root, visibility: visibility)
  if yard_meths.any?
    meths.concat yard_meths
  else
    type = get_namespace_type(namespace, root)
    if type == :class
      meths += yard.get_instance_methods('Object')
    elsif type == :module
      meths += yard.get_instance_methods('Module')
    end
    # TODO: Look out for repeats. Consider not doing this at all.
    sc = get_superclass(namespace, root)
    until sc.nil?
      meths += yard.get_instance_methods(sc, root)
      sc = get_superclass(sc)
    end
  end
  meths
end

#get_instance_variables(namespace, scope = :instance) ⇒ Object



221
222
223
224
225
226
227
228
# File 'lib/solargraph/api_map.rb', line 221

def get_instance_variables(namespace, scope = :instance)
  nodes = get_namespace_nodes(namespace) || @file_nodes.values
  arr = []
  nodes.each { |n|
    arr += inner_get_instance_variables(n, scope)
  }
  arr
end

#get_method_args(node) ⇒ Object



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/solargraph/api_map.rb', line 327

def get_method_args node
  list = nil
  args = []
  node.children.each { |c|
    if c.kind_of?(AST::Node) and c.type == :args
      list = c
      break
    end
  }
  return args if list.nil?
  list.children.each { |c|
    if c.type == :arg
      args.push c.children[0]
    end
  }
  args
end

#get_methods(namespace, root = '', visibility: [:public]) ⇒ Object



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/solargraph/api_map.rb', line 309

def get_methods(namespace, root = '', visibility: [:public])
  meths = []
  meths += inner_get_methods(namespace, root, []) #unless has_yardoc?
  yard = YardMap.new(required: @required, workspace: @workspace)
  yard_meths = yard.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 += yard.get_instance_methods('Class')
    elsif type == :module
      meths += yard.get_methods('Module')
    end
    meths
  end
end

#get_namespace_nodes(fqns) ⇒ Object



216
217
218
219
# File 'lib/solargraph/api_map.rb', line 216

def get_namespace_nodes(fqns)
  return @file_nodes.values if fqns == ''
  @namespace_map[fqns] || []
end

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



299
300
301
302
303
304
305
306
307
# File 'lib/solargraph/api_map.rb', line 299

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_root_for(node) ⇒ Object



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

def get_root_for(node)
  s = @parent_stack[node]
  return nil if s.nil?
  return node if s.empty?
  s.last
end

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



369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/solargraph/api_map.rb', line 369

def get_superclass(namespace, root = '')
  fqns = find_fully_qualified_namespace(namespace, root)
  nodes = get_namespace_nodes(fqns)
  nodes.each { |n|
    if n.kind_of?(AST::Node)
      if n.type == :class and !n.children[1].nil?
        return unpack_name(n.children[1])
      end
    end
  }
  return nil
end

#has_yardoc?Boolean

Returns:

  • (Boolean)


44
45
46
# File 'lib/solargraph/api_map.rb', line 44

def has_yardoc?
  workspace and File.exist?(File.join(workspace, '.yardoc'))
end

#infer_instance_variable(var, namespace, scope = :instance) ⇒ Object



266
267
268
269
270
271
272
273
274
275
# File 'lib/solargraph/api_map.rb', line 266

def infer_instance_variable(var, namespace, scope = :instance)
  vn = nil
  if namespace_exists?(namespace)
    get_namespace_nodes(namespace).each { |node|
      vn = find_instance_variable_assignment(var, node, scope)
      break unless vn.nil?
    }
  end
  infer(vn.children[1]) unless vn.nil?
end

#inner_get_instance_variables(node, scope) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/solargraph/api_map.rb', line 250

def inner_get_instance_variables(node, scope)
  arr = []
  if node.kind_of?(AST::Node)
    node.children.each { |c|
      if c.kind_of?(AST::Node)
        is_inst = !find_parent(c, :def).nil?
        if c.type == :ivasgn and ( (scope == :instance and is_inst) or (scope != :instance and !is_inst) )
          arr.push Suggestion.new(c.children[0], kind: Suggestion::VARIABLE)
        end
        arr += inner_get_instance_variables(c, scope) unless [:class, :module].include?(c.type)
      end
    }
  end
  arr
end

#inner_namespaces_in(name, root, skip) ⇒ Object



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/solargraph/api_map.rb', line 141

def inner_namespaces_in name, root, skip
  result = []
  fqns = find_fully_qualified_namespace(name, root)
  if fqns.nil?
    return result
  else
    return result if skip.include?(fqns)
    skip.push fqns
    nodes = get_namespace_nodes(fqns)
    nodes.delete_if { |n| yardoc_has_file?(get_filename_for(n))}
    return result if nodes.empty?
    cursor = @namespace_tree
    parts = fqns.split('::')
    parts.each { |p|
      cursor = cursor[p]
    }
    unless cursor.nil?
      cursor.keys.each { |k|
        type = get_namespace_type(k, fqns)
        kind = nil
        detail = nil
        if type == :class
          kind = Suggestion::CLASS
          detail = 'Class'
        elsif type == :module
          kind = Suggestion::MODULE
          detail = 'Module'
        end
        result.push Suggestion.new(k, kind: kind, detail: detail)
      }
      nodes = get_namespace_nodes(fqns)
      nodes.each { |n|
        get_include_strings_from(n).each { |i|
          result += inner_namespaces_in(i, fqns, skip)
        }
      }
    end
  end
  result
end

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

Returns:

  • (Boolean)


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

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

#namespacesObject



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

def namespaces
  @namespace_map.keys
end

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

, skip = []



126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/solargraph/api_map.rb', line 126

def namespaces_in name, root = '' #, skip = []
  result = []
  result += inner_namespaces_in(name, root, [])
  yard = YardMap.new(required: @required, workspace: @workspace)
  result += yard.get_constants name, root
  fqns = find_fully_qualified_namespace(name, root)
  unless fqns.nil?
    nodes = get_namespace_nodes(fqns)
    get_include_strings_from(*nodes).each { |i|
      result += yard.get_constants(i, root)
    }
  end
  result
end

#process_mapsObject



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

def process_maps
  @parent_stack = {}
  @namespace_map = {}
  @namespace_tree = {}
  @file_nodes.values.each { |f|
    map_parents f #AST::Node.new(:tmp, @file_nodes.values)
    map_namespaces f #AST::Node.new(:tmp, @file_nodes.values)
  }
end

#yardoc_has_file?(filename) ⇒ Boolean

Returns:

  • (Boolean)


48
49
50
51
52
53
54
55
# File 'lib/solargraph/api_map.rb', line 48

def yardoc_has_file?(filename)
  return false unless has_yardoc?
  return false if filename.nil?
  if filename.start_with?(File.join(workspace, 'lib/'), File.join(workspace, 'app/'))
    return true
  end
  false
end