Class: Solargraph::Source

Inherits:
Object
  • Object
show all
Includes:
EncodingFixes, NodeMethods
Defined in:
lib/solargraph/source.rb,
lib/solargraph/source/chain.rb,
lib/solargraph/source/change.rb,
lib/solargraph/source/cursor.rb,
lib/solargraph/source/updater.rb,
lib/solargraph/source/chain/or.rb,
lib/solargraph/source/chain/call.rb,
lib/solargraph/source/chain/head.rb,
lib/solargraph/source/chain/link.rb,
lib/solargraph/source/node_chainer.rb,
lib/solargraph/source/node_methods.rb,
lib/solargraph/source/chain/literal.rb,
lib/solargraph/source/chain/constant.rb,
lib/solargraph/source/chain/variable.rb,
lib/solargraph/source/encoding_fixes.rb,
lib/solargraph/source/flawed_builder.rb,
lib/solargraph/source/source_chainer.rb,
lib/solargraph/source/chain/class_variable.rb,
lib/solargraph/source/chain/global_variable.rb,
lib/solargraph/source/chain/instance_variable.rb

Overview

A Ruby file that has been parsed into an AST.

Defined Under Namespace

Modules: EncodingFixes, NodeMethods Classes: Chain, Change, Cursor, NodeChainer, SourceChainer, Updater

Constant Summary collapse

FOLDING_NODE_TYPES =
%i[
  class sclass module def defs if str dstr array while unless kwbegin hash block
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from NodeMethods

const_from, drill_signature, get_node_end_position, get_node_start_position, infer_literal_node_type, pack_name, resolve_node_signature, returns_from, unpack_name

Methods included from EncodingFixes

normalize

Constructor Details

#initialize(code, filename = nil, version = 0) ⇒ Source

Returns a new instance of Source

Parameters:

  • code (String)
  • filename (String) (defaults to: nil)
  • version (Integer) (defaults to: 0)

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

def initialize code, filename = nil, version = 0
  @code = normalize(code)
  @repaired = code
  @filename = filename
  @version = version
  @domains = []
  begin
    @node, @comments = Source.parse_with_comments(@code, filename)
    @parsed = true
  rescue Parser::SyntaxError, EncodingError => e
    # @todo 100% whitespace results in a nil node, so there's no reason to parse it.
    #   We still need to determine whether the resulting node should be nil or a dummy
    #   node with a location that encompasses the range.
    # @node, @comments = Source.parse_with_comments(@code.gsub(/[^\s]/, ' '), filename)
    @node = nil
    @comments = []
    @parsed = false
  rescue Exception => e
    Solargraph.logger.warn "[#{e.class}] #{e.message}"
    Solargraph.logger.warn e.backtrace.join("\n")
    raise "Error parsing #{filename || '(source)'}: [#{e.class}] #{e.message}"
  ensure
    @code.freeze
  end
end

Instance Attribute Details

#codeString

Returns:

  • (String)

26
27
28
# File 'lib/solargraph/source.rb', line 26

def code
  @code
end

#commentsArray<Parser::Source::Comment>

Returns:

  • (Array<Parser::Source::Comment>)

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

def comments
  @comments
end

#error_rangesArray<Range>

Returns:


244
245
246
# File 'lib/solargraph/source.rb', line 244

def error_ranges
  @error_ranges ||= []
end

#filenameString

Returns:

  • (String)

23
24
25
# File 'lib/solargraph/source.rb', line 23

def filename
  @filename
end

#nodeParser::AST::Node

Returns:

  • (Parser::AST::Node)

29
30
31
# File 'lib/solargraph/source.rb', line 29

def node
  @node
end

#versionInteger

TODO:

Deprecate?

Returns:

  • (Integer)

36
37
38
# File 'lib/solargraph/source.rb', line 36

def version
  @version
end

Class Method Details

.load(filename) ⇒ Solargraph::Source

Parameters:

  • filename (String)

Returns:


488
489
490
491
492
493
# File 'lib/solargraph/source.rb', line 488

def load filename
  file = File.open(filename)
  code = file.read
  file.close
  Source.load_string(code, filename)
end

.load_string(code, filename = nil, version = 0) ⇒ Solargraph::Source

Parameters:

  • code (String)
  • filename (String) (defaults to: nil)
  • version (Integer) (defaults to: 0)

Returns:


499
500
501
# File 'lib/solargraph/source.rb', line 499

def load_string code, filename = nil, version = 0
  Source.new code, filename, version
end

.parse(code, filename = nil, line = 0) ⇒ Parser::AST::Node

Parameters:

  • code (String)
  • filename (String, nil) (defaults to: nil)
  • line (Integer) (defaults to: 0)

Returns:

  • (Parser::AST::Node)

516
517
518
519
520
# File 'lib/solargraph/source.rb', line 516

def parse code, filename = nil, line = 0
  buffer = Parser::Source::Buffer.new(filename, line)
  buffer.source = code
  parser.parse(buffer)
end

.parse_docstring(comments) ⇒ YARD::DocstringParser

Parameters:

  • comments (String)

Returns:

  • (YARD::DocstringParser)

534
535
536
537
538
# File 'lib/solargraph/source.rb', line 534

def parse_docstring comments
  # HACK: Pass a dummy code object to the parser for plugins that
  # expect it not to be nil
  YARD::Docstring.parser.parse(comments, YARD::CodeObjects::Base.new(:root, 'stub'))
end

.parse_with_comments(code, filename = nil) ⇒ Array(Parser::AST::Node, Array<Parser::Source::Comment>)

Parameters:

  • code (String)
  • filename (String) (defaults to: nil)

Returns:

  • (Array(Parser::AST::Node, Array<Parser::Source::Comment>))

506
507
508
509
510
# File 'lib/solargraph/source.rb', line 506

def parse_with_comments code, filename = nil
  buffer = Parser::Source::Buffer.new(filename, 0)
  buffer.source = code
  parser.parse_with_comments(buffer)
end

.parserParser::Base

Returns:

  • (Parser::Base)

523
524
525
526
527
528
529
530
# File 'lib/solargraph/source.rb', line 523

def parser
  # @todo Consider setting an instance variable. We might not need to
  #   recreate the parser every time we use it.
  parser = Parser::CurrentRuby.new(FlawedBuilder.new)
  parser.diagnostics.all_errors_are_fatal = true
  parser.diagnostics.ignore_warnings      = true
  parser
end

Instance Method Details

#associated_commentsHash{Integer => Array<Parser::Source::Comment>}

Get a hash of comments grouped by the line numbers of the associated code.

Returns:

  • (Hash{Integer => Array<Parser::Source::Comment>})

303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/solargraph/source.rb', line 303

def associated_comments
  @associated_comments ||= begin
    result = {}
    Parser::Source::Comment.associate_locations(node, comments).each_pair do |loc, all|
      block = all.select { |l| l.loc.line < loc.line }
      next if block.empty?
      result[loc.line] ||= []
      result[loc.line].concat block
    end
    result
  end
end

#at(range) ⇒ String

Parameters:

Returns:

  • (String)

69
70
71
# File 'lib/solargraph/source.rb', line 69

def at range
  from_to range.start.line, range.start.character, range.ending.line, range.ending.character
end

#code_for(node) ⇒ String

Parameters:

  • node (Parser::AST::Node)

Returns:

  • (String)

250
251
252
253
254
255
# File 'lib/solargraph/source.rb', line 250

def code_for(node)
  b = Position.line_char_to_offset(@code, node.location.line, node.location.column)
  e = Position.line_char_to_offset(@code, node.location.last_line, node.location.last_column)
  frag = code[b..e-1].to_s
  frag.strip.gsub(/,$/, '')
end

#comment_at?(position) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)

217
218
219
220
221
222
223
224
# File 'lib/solargraph/source.rb', line 217

def comment_at? position
  comment_ranges.each do |range|
    return true if range.include?(position) ||
      (range.ending.line == position.line && range.ending.column < position.column)
    break if range.ending.line > position.line
  end
  false
end

#comments_for(node) ⇒ String

Parameters:

  • node (Parser::AST::Node)

Returns:

  • (String)

259
260
261
262
263
264
# File 'lib/solargraph/source.rb', line 259

def comments_for node
  stringified_comments[node.loc.line] ||= begin
    arr = associated_comments[node.loc.line]
    arr ? stringify_comment_array(arr) : nil
  end
end

#cursor_at(position) ⇒ Source::Cursor

Parameters:

Returns:


178
179
180
# File 'lib/solargraph/source.rb', line 178

def cursor_at position
  Cursor.new(self, position)
end

#finish_synchronizeSource

Finish synchronizing a source that was updated via #start_synchronize. This method returns self if the source is already synchronized. Otherwise it parses the AST and returns a new synchronized Source.

Returns:


136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/solargraph/source.rb', line 136

def finish_synchronize
  return self if synchronized?
  synced = Source.new(@code, filename)
  if synced.parsed?
    synced.version = version
    return synced
  end
  synced = Source.new(@repaired, filename)
  synced.error_ranges.concat (error_ranges + last_updater.changes.map(&:range))
  synced.code = @code
  synced.synchronized = true
  synced.version = version
  synced
end

#folding_rangesArray<Range>

Get an array of ranges that can be folded, e.g., the range of a class definition or an if condition.

See FOLDING_NODE_TYPES for the list of node types that can be folded.

Returns:


286
287
288
289
290
291
292
293
# File 'lib/solargraph/source.rb', line 286

def folding_ranges
  @folding_ranges ||= begin
    result = []
    inner_folding_ranges node, result
    result.concat foldable_comment_block_ranges
    result
  end
end

#from_to(l1, c1, l2, c2) ⇒ String

Parameters:

  • l1 (Integer)
  • c1 (Integer)
  • l2 (Integer)
  • c2 (Integer)

Returns:

  • (String)

78
79
80
81
82
# File 'lib/solargraph/source.rb', line 78

def from_to l1, c1, l2, c2
  b = Solargraph::Position.line_char_to_offset(@code, l1, c1)
  e = Solargraph::Position.line_char_to_offset(@code, l2, c2)
  @code[b..e-1]
end

#locationLocation

A location representing the file in its entirety.

Returns:


269
270
271
272
273
274
# File 'lib/solargraph/source.rb', line 269

def location
  st = Position.new(0, 0)
  en = Position.from_offset(code, code.length)
  range = Range.new(st, en)
  Location.new(filename, range)
end

#node_at(line, column) ⇒ AST::Node

Get the nearest node that contains the specified index.

Parameters:

  • line (Integer)
  • column (Integer)

Returns:

  • (AST::Node)

89
90
91
# File 'lib/solargraph/source.rb', line 89

def node_at(line, column)
  tree_at(line, column).first
end

#parsed?Boolean

Returns:

  • (Boolean)

183
184
185
# File 'lib/solargraph/source.rb', line 183

def parsed?
  @parsed
end

#references(name) ⇒ Array<Location>

Parameters:

  • name (String)

Returns:


228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/solargraph/source.rb', line 228

def references name
  inner_node_references(name, node).map do |n|
    offset = Position.to_offset(code, get_node_start_position(n))
    soff = code.index(name, offset)
    eoff = soff + name.length
    Location.new(
      filename,
      Range.new(
        Position.from_offset(code, soff),
        Position.from_offset(code, eoff)
      )
    )
  end
end

#repaired?Boolean

Returns:

  • (Boolean)

187
188
189
# File 'lib/solargraph/source.rb', line 187

def repaired?
  @is_repaired ||= (@code != @repaired)
end

#start_synchronize(updater) ⇒ Source

Start synchronizing the source. This method updates the code without parsing a new AST. The resulting Source object will be marked not synchronized (#synchronized? == false).

Parameters:

Returns:


113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/solargraph/source.rb', line 113

def start_synchronize updater
  raise 'Invalid synchronization' unless updater.filename == filename
  real_code = updater.write(@code)
  src = Source.allocate
  src.filename = filename
  src.code = real_code
  src.version = updater.version
  src.parsed = parsed?
  src.repaired = updater.repair(@repaired)
  src.synchronized = false
  src.node = @node
  src.comments = @comments
  src.error_ranges = error_ranges
  src.last_updater = updater
  return src.finish_synchronize unless real_code.lines.length == @code.lines.length
  src
end

#string_at?(position) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)

193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/solargraph/source.rb', line 193

def string_at? position
  return false if Position.to_offset(code, position) >= code.length
  string_nodes.each do |node|
    range = Range.from_node(node)
    next if range.ending.line < position.line
    break if range.ending.line > position.line
    return true if node.type == :str && range.include?(position) && range.start != position
    if node.type == :dstr
      inner = node_at(position.line, position.column)
      next if inner.nil?
      inner_range = Range.from_node(inner)
      next unless range.include?(inner_range.ending)
      return true if inner.type == :str
      inner_code = at(Solargraph::Range.new(inner_range.start, position))
      return true if (inner.type == :dstr && inner_range.ending.character <= position.character) && !inner_code.end_with?('}') ||
        (inner.type != :dstr && inner_range.ending.line == position.line && position.character <= inner_range.ending.character && inner_code.end_with?('}'))
    end
    break if range.ending.line > position.line
  end
  false
end

#synchronize(updater) ⇒ Source

Synchronize the Source with an update. This method applies changes to the code, parses the new code's AST, and returns the resulting Source object.

Parameters:

Returns:


156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/solargraph/source.rb', line 156

def synchronize updater
  raise 'Invalid synchronization' unless updater.filename == filename
  real_code = updater.write(@code)
  if real_code == @code
    @version = updater.version
    return self
  end
  synced = Source.new(real_code, filename)
  if synced.parsed?
    synced.version = updater.version
    return synced
  end
  incr_code = updater.repair(@repaired)
  synced = Source.new(incr_code, filename)
  synced.error_ranges.concat (error_ranges + updater.changes.map(&:range))
  synced.code = real_code
  synced.version = updater.version
  synced
end

#synchronized?Boolean

Returns:

  • (Boolean)

295
296
297
298
# File 'lib/solargraph/source.rb', line 295

def synchronized?
  @synchronized = true if @synchronized.nil?
  @synchronized
end

#tree_at(line, column) ⇒ Array<AST::Node>

Get an array of nodes containing the specified index, starting with the nearest node and ending with the root.

Parameters:

  • line (Integer)
  • column (Integer)

Returns:

  • (Array<AST::Node>)

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

def tree_at(line, column)
  # offset = Position.line_char_to_offset(@code, line, column)
  position = Position.new(line, column)
  stack = []
  inner_tree_at @node, position, stack
  stack
end