ruby_language_server 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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