ruby_language_server 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.txt +67 -0
  3. data/FAQ_ROADMAP.md +30 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +111 -0
  6. data/Guardfile +36 -0
  7. data/LICENSE +21 -0
  8. data/Makefile +35 -0
  9. data/README.md +55 -0
  10. data/Rakefile +17 -0
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/exe/ruby_language_server +9 -0
  14. data/lib/ruby_language_server/code_file.rb +129 -0
  15. data/lib/ruby_language_server/completion.rb +87 -0
  16. data/lib/ruby_language_server/gem_installer.rb +24 -0
  17. data/lib/ruby_language_server/good_cop.rb +125 -0
  18. data/lib/ruby_language_server/io.rb +130 -0
  19. data/lib/ruby_language_server/line_context.rb +39 -0
  20. data/lib/ruby_language_server/location.rb +29 -0
  21. data/lib/ruby_language_server/logger.rb +14 -0
  22. data/lib/ruby_language_server/project_manager.rb +231 -0
  23. data/lib/ruby_language_server/scope_data/base.rb +23 -0
  24. data/lib/ruby_language_server/scope_data/scope.rb +104 -0
  25. data/lib/ruby_language_server/scope_data/variable.rb +25 -0
  26. data/lib/ruby_language_server/scope_parser.rb +334 -0
  27. data/lib/ruby_language_server/scope_parser_commands/rails_commands.rb +29 -0
  28. data/lib/ruby_language_server/scope_parser_commands/rake_commands.rb +31 -0
  29. data/lib/ruby_language_server/scope_parser_commands/readme.txt +9 -0
  30. data/lib/ruby_language_server/scope_parser_commands/rspec_commands.rb +26 -0
  31. data/lib/ruby_language_server/scope_parser_commands/ruby_commands.rb +70 -0
  32. data/lib/ruby_language_server/server.rb +123 -0
  33. data/lib/ruby_language_server/version.rb +5 -0
  34. data/lib/ruby_language_server.rb +14 -0
  35. data/ruby_language_server.gemspec +56 -0
  36. metadata +293 -0
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module RubyLanguageServer
6
+ class GoodCop < RuboCop::Runner
7
+ CONFIG_PATH = '/project/.rubocop.yml'
8
+ FALLBACK_PATH = '/app/.rubocop.yml'
9
+ @initialization_error = nil
10
+
11
+ def initialize
12
+ config_store = RuboCop::ConfigStore.new
13
+ config_store.options_config =
14
+ if File.exist?(CONFIG_PATH)
15
+ CONFIG_PATH
16
+ else
17
+ FALLBACK_PATH
18
+ end
19
+ super({}, config_store)
20
+ rescue Exception => exception
21
+ RubyLanguageServer.logger.error(exception)
22
+ @initialization_error = "There was an issue loading the rubocop configuration file: #{exception}. Maybe you need to add some additional gems to the ide-ruby settings?"
23
+ end
24
+
25
+ # namespace DiagnosticSeverity {
26
+ # /**
27
+ # * Reports an error.
28
+ # */
29
+ # export const Error = 1;
30
+ # /**
31
+ # * Reports a warning.
32
+ # */
33
+ # export const Warning = 2;
34
+ # /**
35
+ # * Reports an information.
36
+ # */
37
+ # export const Information = 3;
38
+ # /**
39
+ # * Reports a hint.
40
+ # */
41
+ # export const Hint = 4;
42
+ # }
43
+
44
+ # interface Diagnostic {
45
+ # /**
46
+ # * The range at which the message applies.
47
+ # */
48
+ # range: Range;
49
+ #
50
+ # /**
51
+ # * The diagnostic's severity. Can be omitted. If omitted it is up to the
52
+ # * client to interpret diagnostics as error, warning, info or hint.
53
+ # */
54
+ # severity?: number;
55
+ #
56
+ # /**
57
+ # * The diagnostic's code. Can be omitted.
58
+ # */
59
+ # code?: number | string;
60
+ #
61
+ # /**
62
+ # * A human-readable string describing the source of this
63
+ # * diagnostic, e.g. 'typescript' or 'super lint'.
64
+ # */
65
+ # source?: string;
66
+ #
67
+ # /**
68
+ # * The diagnostic's message.
69
+ # */
70
+ # message: string;
71
+ # }
72
+
73
+ def diagnostic_severity_for(severity)
74
+ case severity.to_s
75
+ when 'error', 'fatal'
76
+ 1
77
+ when 'warning'
78
+ 2
79
+ when 'refactor', 'convention'
80
+ 3
81
+ else
82
+ RubyLanguageServer.logger.warn("Could not map severity for #{severity} - returning 2")
83
+ 2
84
+ end
85
+ end
86
+
87
+ def diagnostics(text, filename = nil)
88
+ return initialization_offenses unless @initialization_error.nil?
89
+
90
+ maximum_severity = (ENV['LINT_LEVEL'] || 4).to_i
91
+ enabled_offenses = offenses(text, filename).reject { |offense| offense.status == :disabled }
92
+ enabled_offenses.map do |offense|
93
+ {
94
+ range: Location.position_hash(offense.location.line, offense.location.column, offense.location.last_line, offense.location.last_column),
95
+ severity: diagnostic_severity_for(offense.severity),
96
+ # code?: number | string;
97
+ code: 'code',
98
+ source: "RuboCop:#{offense.cop_name}",
99
+ message: offense.message
100
+ }
101
+ end.select { |hash| hash[:severity] <= maximum_severity }
102
+ end
103
+
104
+ private
105
+
106
+ def offenses(text, filename)
107
+ processed_source = RuboCop::ProcessedSource.new(text, 2.4, filename)
108
+ offenses = inspect_file(processed_source)
109
+ offenses.compact.flatten
110
+ end
111
+
112
+ def initialization_offenses
113
+ [
114
+ {
115
+ range: Location.position_hash(1, 1, 1, 1),
116
+ severity: 'startup', # diagnostic_severity_for(offense.severity),
117
+ # code?: number | string;
118
+ code: 'code',
119
+ source: 'RuboCop:RubyLanguageServer',
120
+ message: @initialization_error
121
+ }
122
+ ]
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module RubyLanguageServer
6
+ class IO
7
+ def initialize(server)
8
+ @server = server
9
+ server.io = self
10
+ loop do
11
+ (id, response) = process_request(STDIN)
12
+ return_response(id, response, STDOUT) unless id.nil?
13
+ rescue SignalException => e
14
+ RubyLanguageServer.logger.error "We received a signal. Let's bail: #{e}"
15
+ exit(true)
16
+ rescue Exception => e
17
+ RubyLanguageServer.logger.error "Something when horribly wrong: #{e}"
18
+ backtrace = e.backtrace * "\n"
19
+ RubyLanguageServer.logger.error "Backtrace:\n#{backtrace}"
20
+ end
21
+ end
22
+
23
+ def return_response(id, response, io = STDOUT)
24
+ full_response = {
25
+ jsonrpc: '2.0',
26
+ id: id,
27
+ result: response
28
+ }
29
+ response_body = JSON.unparse(full_response)
30
+ # RubyLanguageServer.logger.debug "response_body: #{response_body}"
31
+ io.write "Content-Length: #{response_body.length + 0}\r\n"
32
+ io.write "\r\n"
33
+ io.write response_body
34
+ io.flush
35
+ end
36
+
37
+ def send_notification(message, params, io = STDOUT)
38
+ full_response = {
39
+ jsonrpc: '2.0',
40
+ method: message,
41
+ params: params
42
+ }
43
+ body = JSON.unparse(full_response)
44
+ # RubyLanguageServer.logger.debug "body: #{body}"
45
+ io.write "Content-Length: #{body.length + 0}\r\n"
46
+ io.write "\r\n"
47
+ io.write body
48
+ io.flush
49
+ end
50
+
51
+ def process_request(io = STDIN)
52
+ request_body = get_request(io)
53
+ # RubyLanguageServer.logger.debug "request_body: #{request_body}"
54
+ request_json = JSON.parse request_body
55
+ id = request_json['id']
56
+ method_name = request_json['method']
57
+ params = request_json['params']
58
+ method_name = "on_#{method_name.gsub(/[^\w]/, '_')}"
59
+ if @server.respond_to? method_name
60
+ response = @server.send(method_name, params)
61
+ exit(true) if response == 'EXIT'
62
+ return id, response
63
+ else
64
+ RubyLanguageServer.logger.warn "SERVER DOES NOT RESPOND TO #{method_name}"
65
+ return nil
66
+ end
67
+ end
68
+
69
+ def get_request(io = STDIN)
70
+ initial_line = get_initial_request_line(io)
71
+ RubyLanguageServer.logger.debug "initial_line: #{initial_line}"
72
+ length = get_length(initial_line)
73
+ content = ''
74
+ while content.length < length + 2
75
+ begin
76
+ content += get_content(length + 2, io) # Why + 2? CRLF?
77
+ rescue Exception => exception
78
+ RubyLanguageServer.logger.error exception
79
+ # We have almost certainly been disconnected from the server
80
+ exit!(1)
81
+ end
82
+ end
83
+ RubyLanguageServer.logger.debug "content.length: #{content.length}"
84
+ content
85
+ end
86
+
87
+ def get_initial_request_line(io = STDIN)
88
+ io.gets
89
+ end
90
+
91
+ def get_length(string)
92
+ return 0 if string.nil?
93
+
94
+ string.match(/Content-Length: (\d+)/)[1].to_i
95
+ end
96
+
97
+ def get_content(size, io = STDIN)
98
+ io.read(size)
99
+ end
100
+
101
+ # http://www.alecjacobson.com/weblog/?p=75
102
+ # def stdin_read_char
103
+ # begin
104
+ # # save previous state of stty
105
+ # old_state = `stty -g`
106
+ # # disable echoing and enable raw (not having to press enter)
107
+ # system "stty raw -echo"
108
+ # c = STDIN.getc.chr
109
+ # # gather next two characters of special keys
110
+ # if(c=="\e")
111
+ # extra_thread = Thread.new{
112
+ # c = c + STDIN.getc.chr
113
+ # c = c + STDIN.getc.chr
114
+ # }
115
+ # # wait just long enough for special keys to get swallowed
116
+ # extra_thread.join(0.00001)
117
+ # # kill thread so not-so-long special keys don't wait on getc
118
+ # extra_thread.kill
119
+ # end
120
+ # rescue Exception => ex
121
+ # puts "#{ex.class}: #{ex.message}"
122
+ # puts ex.backtrace
123
+ # ensure
124
+ # # restore previous state of stty
125
+ # system "stty #{old_state}"
126
+ # end
127
+ # return c
128
+ # end
129
+ end # class
130
+ end # module
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The purpose of this class is to parse a single line and return an array of the
4
+ # context words given the position of the cursor.
5
+
6
+ # FooModule::BarModule.method_name.ivar = some_var.some_method
7
+ # ^
8
+ # ['FooModule', 'BarModule', 'method_name']
9
+ # ^
10
+ # ['some_var']
11
+
12
+ # If the first part of the context is :: then it returns a leading nil in the array
13
+
14
+ # ::FooModule
15
+ # ^
16
+ # [nil, 'FooModule']
17
+
18
+ module RubyLanguageServer
19
+ module LineContext
20
+ def self.for(line, position)
21
+ # Grab just the last part of the line - from the index onward
22
+ line_end = line[position..-1]
23
+ return nil if line_end.nil?
24
+
25
+ # Grab the portion of the word that starts at the position toward the end of the line
26
+ match = line_end.partition(/^(@{0,2}\w+)/)[1]
27
+ RubyLanguageServer.logger.debug("match: #{match}")
28
+ # Get the start of the line to the end of the matched word
29
+ line_start = line[0..(position + match.length - 1)]
30
+ RubyLanguageServer.logger.debug("line_start: #{line_start}")
31
+ # Match as much as we can to the end of the line - which is now the end of the word
32
+ end_match = line_start.partition(/(@{0,2}[:\.\w]+)$/)[1]
33
+ matches = end_match.split('.', -1)
34
+ matches = matches.map { |m| m.length.positive? ? m.split('::', -1) : m }.flatten
35
+ RubyLanguageServer.logger.debug("matches: #{matches}")
36
+ matches
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLanguageServer
4
+ # Hash factories for the language server
5
+ module Location
6
+ def self.hash(uri, start_line, start_character = 1, end_line = nil, end_character = nil)
7
+ {
8
+ uri: uri,
9
+ range: position_hash(start_line, start_character, end_line, end_character)
10
+ }
11
+ end
12
+
13
+ def self.position_hash(start_line, start_character = 1, end_line = nil, end_character = nil)
14
+ end_line ||= start_line
15
+ end_character ||= start_character
16
+ {
17
+ start:
18
+ {
19
+ line: start_line - 1,
20
+ character: start_character
21
+ },
22
+ 'end': {
23
+ line: end_line - 1,
24
+ character: end_character
25
+ }
26
+ }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module RubyLanguageServer
6
+ level_name = ENV.fetch('LOG_LEVEL') { 'info' }.upcase
7
+ # level_name = 'DEBUG'
8
+ level = Logger::Severity.const_get(level_name)
9
+ @logger = ::Logger.new(STDERR, level: level)
10
+
11
+ def self.logger
12
+ @logger
13
+ end
14
+ end
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fuzzy_match'
4
+ require 'amatch' # note that you have to require this... fuzzy_match won't require it for you
5
+ FuzzyMatch.engine = :amatch # This should be in a config somewhere
6
+
7
+ module RubyLanguageServer
8
+ class ProjectManager
9
+ attr_reader :uri_code_file_hash
10
+
11
+ def initialize(uri)
12
+ @root_path = uri
13
+ @root_uri = "file://#{@root_path}"
14
+ # This is {uri: code_file} where content stuff is like
15
+ @uri_code_file_hash = {}
16
+ @update_mutext = Mutex.new
17
+
18
+ @additional_gems_installed = false
19
+ @additional_gem_mutex = Mutex.new
20
+
21
+ scan_all_project_files
22
+ end
23
+
24
+ def diagnostics_ready?
25
+ @additional_gem_mutex.synchronize { @additional_gems_installed }
26
+ end
27
+
28
+ def install_additional_gems(gem_names)
29
+ Thread.new do
30
+ RubyLanguageServer::GemInstaller.install_gems(gem_names)
31
+ @additional_gem_mutex.synchronize { @additional_gems_installed = true }
32
+ end
33
+ end
34
+
35
+ def text_for_uri(uri)
36
+ code_file = code_file_for_uri(uri)
37
+ code_file&.text || ''
38
+ end
39
+
40
+ def code_file_for_uri(uri, text = nil)
41
+ code_file = @uri_code_file_hash[uri]
42
+ code_file = @uri_code_file_hash[uri] = CodeFile.new(uri, text) if code_file.nil?
43
+ code_file
44
+ end
45
+
46
+ def tags_for_uri(uri)
47
+ code_file = code_file_for_uri(uri)
48
+ return {} if code_file.nil?
49
+
50
+ code_file.tags
51
+ end
52
+
53
+ def root_scope_for(uri)
54
+ code_file = code_file_for_uri(uri)
55
+ RubyLanguageServer.logger.error('code_file.nil?!!!!!!!!!!!!!!') if code_file.nil?
56
+ code_file&.root_scope
57
+ end
58
+
59
+ def all_scopes
60
+ @uri_code_file_hash.values.map(&:root_scope).map(&:self_and_descendants).flatten
61
+ end
62
+
63
+ # Return the list of scopes [deepest, parent, ..., Object]
64
+ def scopes_at(uri, position)
65
+ root_scope = root_scope_for(uri)
66
+ root_scope.scopes_at(position)
67
+ end
68
+
69
+ def completion_at(uri, position)
70
+ relative_position = position.dup
71
+ relative_position.character = relative_position.character # To get before the . or ::
72
+ # RubyLanguageServer.logger.debug("relative_position #{relative_position}")
73
+ RubyLanguageServer.logger.debug("scopes_at(uri, position) #{scopes_at(uri, position).map(&:name)}")
74
+ context_scope = scopes_at(uri, position).first || root_scope_for(uri)
75
+ context = context_at_location(uri, relative_position)
76
+ return {} if context.nil? || context == ''
77
+
78
+ RubyLanguageServer::Completion.completion(context, context_scope, all_scopes)
79
+ end
80
+
81
+ # interface CompletionItem {
82
+ # /**
83
+ # * The label of this completion item. By default
84
+ # * also the text that is inserted when selecting
85
+ # * this completion.
86
+ # */
87
+ # label: string;
88
+ # /**
89
+ # * The kind of this completion item. Based of the kind
90
+ # * an icon is chosen by the editor.
91
+ # */
92
+ # kind?: number;
93
+ # /**
94
+ # * A human-readable string with additional information
95
+ # * about this item, like type or symbol information.
96
+ # */
97
+ # detail?: string;
98
+ # /**
99
+ # * A human-readable string that represents a doc-comment.
100
+ # */
101
+ # documentation?: string;
102
+ # /**
103
+ # * A string that shoud be used when comparing this item
104
+ # * with other items. When `falsy` the label is used.
105
+ # */
106
+ # sortText?: string;
107
+ # /**
108
+ # * A string that should be used when filtering a set of
109
+ # * completion items. When `falsy` the label is used.
110
+ # */
111
+ # filterText?: string;
112
+ # /**
113
+ # * A string that should be inserted a document when selecting
114
+ # * this completion. When `falsy` the label is used.
115
+ # */
116
+ # insertText?: string;
117
+ # /**
118
+ # * The format of the insert text. The format applies to both the `insertText` property
119
+ # * and the `newText` property of a provided `textEdit`.
120
+ # */
121
+ # insertTextFormat?: InsertTextFormat;
122
+ # /**
123
+ # * An edit which is applied to a document when selecting this completion. When an edit is provided the value of
124
+ # * `insertText` is ignored.
125
+ # *
126
+ # * *Note:* The range of the edit must be a single line range and it must contain the position at which completion
127
+ # * has been requested.
128
+ # */
129
+ # textEdit?: TextEdit;
130
+ # /**
131
+ # * An optional array of additional text edits that are applied when
132
+ # * selecting this completion. Edits must not overlap with the main edit
133
+ # * nor with themselves.
134
+ # */
135
+ # additionalTextEdits?: TextEdit[];
136
+ # /**
137
+ # * An optional set of characters that when pressed while this completion is active will accept it first and
138
+ # * then type that character. *Note* that all commit characters should have `length=1` and that superfluous
139
+ # * characters will be ignored.
140
+ # */
141
+ # commitCharacters?: string[];
142
+ # /**
143
+ # * An optional command that is executed *after* inserting this completion. *Note* that
144
+ # * additional modifications to the current document should be described with the
145
+ # * additionalTextEdits-property.
146
+ # */
147
+ # command?: Command;
148
+ # /**
149
+ # * An data entry field that is preserved on a completion item between
150
+ # * a completion and a completion resolve request.
151
+ # */
152
+ # data?: any
153
+ # }
154
+
155
+ def scan_all_project_files
156
+ project_ruby_files = Dir.glob('/project/**/*.rb')
157
+ RubyLanguageServer.logger.debug("scan_all_project_files: #{project_ruby_files * ','}")
158
+ Thread.new do
159
+ project_ruby_files.each do |container_path|
160
+ text = File.read(container_path)
161
+ relative_path = container_path.delete_prefix('/project/')
162
+ host_uri = @root_uri + relative_path
163
+ update_document_content(host_uri, text)
164
+ end
165
+ end
166
+ end
167
+
168
+ def update_document_content(uri, text)
169
+ @update_mutext.synchronize do
170
+ RubyLanguageServer.logger.debug("update_document_content: #{uri}")
171
+ # RubyLanguageServer.logger.error("@root_path: #{@root_path}")
172
+ code_file = code_file_for_uri(uri, text)
173
+ code_file.text = text
174
+ diagnostics_ready? ? code_file.diagnostics : []
175
+ end
176
+ end
177
+
178
+ def context_at_location(uri, position)
179
+ lines = text_for_uri(uri).split("\n")
180
+ line = lines[position.line]
181
+ return [] if line.nil? || line.strip.length.zero?
182
+
183
+ contexts = LineContext.for(line, position.character)
184
+ RubyLanguageServer.logger.debug("LineContext.for(line, position.character): #{contexts}")
185
+ contexts
186
+ end
187
+
188
+ def word_at_location(uri, position)
189
+ context_at_location(uri, position).last
190
+ end
191
+
192
+ def possible_definitions(uri, position)
193
+ name = word_at_location(uri, position)
194
+ return {} if name == ''
195
+
196
+ name = 'initialize' if name == 'new'
197
+ scope = scopes_at(uri, position).first
198
+ results = scope_definitions_for(name, scope, uri)
199
+ return results unless results.empty?
200
+
201
+ project_definitions_for(name, scope)
202
+ end
203
+
204
+ def scope_definitions_for(name, scope, uri)
205
+ check_scope = scope
206
+ return_array = []
207
+ while check_scope
208
+ scope.variables.each do |variable|
209
+ return_array << Location.hash(uri, variable.line) if variable.name == name
210
+ end
211
+ check_scope = check_scope.parent
212
+ end
213
+ RubyLanguageServer.logger.debug("scope_definitions_for(#{name}, #{scope}, #{uri}: #{return_array.uniq})")
214
+ return_array.uniq
215
+ end
216
+
217
+ def project_definitions_for(name, _scope)
218
+ return_array = @uri_code_file_hash.keys.each_with_object([]) do |uri, ary|
219
+ tags = tags_for_uri(uri)
220
+ RubyLanguageServer.logger.debug("tags_for_uri(#{uri}): #{tags_for_uri(uri)}")
221
+ next if tags.nil?
222
+
223
+ match_tags = tags.select { |tag| tag[:name] == name }
224
+ match_tags.each do |tag|
225
+ ary << Location.hash(uri, tag[:location][:range][:start][:line] + 1)
226
+ end
227
+ end
228
+ return_array
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLanguageServer
4
+ module ScopeData
5
+ class Base
6
+ TYPE_MODULE = :module
7
+ TYPE_CLASS = :class
8
+ TYPE_METHOD = :method
9
+ TYPE_BLOCK = :block
10
+ TYPE_ROOT = :root
11
+ TYPE_VARIABLE = :variable
12
+
13
+ JoinHash = {
14
+ TYPE_MODULE => '::',
15
+ TYPE_CLASS => '::',
16
+ TYPE_METHOD => '#',
17
+ TYPE_BLOCK => '>',
18
+ TYPE_ROOT => '',
19
+ TYPE_VARIABLE => '^'
20
+ }.freeze
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLanguageServer
4
+ module ScopeData
5
+ # The Scope class is basically a container with context.
6
+ # It is used to track top & bottom line, variables in this scope, contanst, and children - which could be functions, classes, blocks, etc. Anything that adds scope.
7
+ # Remember, this is scope for a file. It seems reasonabble that this will get used less in the future when we know more about classes.
8
+ class Scope < Base
9
+ include Enumerable
10
+
11
+ attr_accessor :top_line # first line
12
+ attr_accessor :bottom_line # last line
13
+ attr_accessor :depth # how many parent scopes
14
+ attr_accessor :parent # parent scope
15
+ attr_accessor :variables # variables declared in this scope
16
+ attr_accessor :constants # constants declared in this scope
17
+ attr_accessor :children # child scopes
18
+ attr_accessor :type # Type of this scope (module, class, block)
19
+ attr_accessor :name # method
20
+ attr_accessor :superclass_name # superclass name
21
+
22
+ def initialize(parent = nil, type = TYPE_ROOT, name = '', top_line = 1, _column = 1)
23
+ super()
24
+ @parent = parent
25
+ @type = type
26
+ @name = name
27
+ @top_line = top_line
28
+ @depth = parent.nil? ? 0 : parent.depth + 1
29
+ @full_name = [parent ? parent.full_name : nil, @name].compact.join(JoinHash[type]) unless type == TYPE_ROOT
30
+ @children = []
31
+ @variables = []
32
+ @constants = []
33
+ end
34
+
35
+ def inspect
36
+ "Scope: #{@name} (#{@full_name}) #{@top_line}-#{@bottom_line} children: #{@children} vars: #{@variables}"
37
+ end
38
+
39
+ def pretty_print(pp) # rubocop:disable Naming/UncommunicativeMethodParamName
40
+ {
41
+ Scope: {
42
+ type: type,
43
+ name: name,
44
+ lines: [@top_line, @bottom_line],
45
+ children: children,
46
+ variables: variables
47
+ }
48
+ }.pretty_print(pp)
49
+ end
50
+
51
+ def full_name
52
+ @full_name || @name
53
+ end
54
+
55
+ def has_variable_or_constant?(variable) # rubocop:disable Naming/PredicateName
56
+ test_array = variable.constant? ? constants : variables
57
+ matching_variable = test_array.detect { |test_variable| (test_variable.name == variable.name) }
58
+ !matching_variable.nil?
59
+ end
60
+
61
+ # Return the deepest child scopes of this scope - and on up.
62
+ # Not done recuresively because we don't really need to.
63
+ # Normally called on a root scope.
64
+ def scopes_at(position)
65
+ line = position.line
66
+ matching_scopes = select do |scope|
67
+ scope.top_line && scope.bottom_line && (scope.top_line..scope.bottom_line).cover?(line)
68
+ end
69
+ return [] if matching_scopes == []
70
+
71
+ deepest_scope = matching_scopes.max_by(&:depth)
72
+ deepest_scope.self_and_ancestors
73
+ end
74
+
75
+ def each
76
+ self_and_descendants.each { |member| yield member }
77
+ end
78
+
79
+ # Self and all descendents flattened into array
80
+ def self_and_descendants
81
+ [self] + descendants
82
+ end
83
+
84
+ def descendants
85
+ children.map(&:self_and_descendants).flatten
86
+ end
87
+
88
+ # [self, parent, parent.parent...]
89
+ def self_and_ancestors
90
+ return [self, parent.self_and_ancestors].flatten unless parent.nil?
91
+
92
+ [self]
93
+ end
94
+
95
+ def set_superclass_name(partial)
96
+ if partial.start_with?('::')
97
+ @superclass_name = partial.gsub(/^::/, '')
98
+ else
99
+ @superclass_name = [parent ? parent.full_name : nil, partial].compact.join(JoinHash[type])
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end