holistic-ruby 0.1.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +35 -0
- data/Rakefile +8 -0
- data/config/logging.rb +6 -0
- data/exe/holistic-ruby +6 -0
- data/holistic-ruby.gemspec +34 -0
- data/lib/holistic/application.rb +29 -0
- data/lib/holistic/background_process.rb +11 -0
- data/lib/holistic/database/table.rb +78 -0
- data/lib/holistic/document/cursor.rb +9 -0
- data/lib/holistic/document/file.rb +36 -0
- data/lib/holistic/document/location.rb +35 -0
- data/lib/holistic/document/unsaved/change.rb +24 -0
- data/lib/holistic/document/unsaved/collection.rb +21 -0
- data/lib/holistic/document/unsaved/record.rb +83 -0
- data/lib/holistic/extensions/events.rb +37 -0
- data/lib/holistic/extensions/ruby/stdlib.rb +43 -0
- data/lib/holistic/language_server/current.rb +11 -0
- data/lib/holistic/language_server/format/file_uri.rb +19 -0
- data/lib/holistic/language_server/lifecycle.rb +59 -0
- data/lib/holistic/language_server/message.rb +21 -0
- data/lib/holistic/language_server/protocol.rb +45 -0
- data/lib/holistic/language_server/request.rb +21 -0
- data/lib/holistic/language_server/requests/lifecycle/exit.rb +10 -0
- data/lib/holistic/language_server/requests/lifecycle/initialize.rb +75 -0
- data/lib/holistic/language_server/requests/lifecycle/initialized.rb +13 -0
- data/lib/holistic/language_server/requests/lifecycle/shutdown.rb +14 -0
- data/lib/holistic/language_server/requests/text_document/completion.rb +68 -0
- data/lib/holistic/language_server/requests/text_document/did_change.rb +30 -0
- data/lib/holistic/language_server/requests/text_document/did_close.rb +33 -0
- data/lib/holistic/language_server/requests/text_document/did_open.rb +16 -0
- data/lib/holistic/language_server/requests/text_document/did_save.rb +33 -0
- data/lib/holistic/language_server/requests/text_document/find_references.rb +52 -0
- data/lib/holistic/language_server/requests/text_document/go_to_definition.rb +64 -0
- data/lib/holistic/language_server/response.rb +39 -0
- data/lib/holistic/language_server/router.rb +48 -0
- data/lib/holistic/language_server/stdio/parser.rb +65 -0
- data/lib/holistic/language_server/stdio/server.rb +46 -0
- data/lib/holistic/language_server/stdio/start.rb +48 -0
- data/lib/holistic/ruby/autocompletion/suggest.rb +75 -0
- data/lib/holistic/ruby/parser/constant_resolution.rb +61 -0
- data/lib/holistic/ruby/parser/live_editing/process_file_changed.rb +62 -0
- data/lib/holistic/ruby/parser/nesting_syntax.rb +76 -0
- data/lib/holistic/ruby/parser/program_visitor.rb +205 -0
- data/lib/holistic/ruby/parser/table_of_contents.rb +17 -0
- data/lib/holistic/ruby/parser.rb +26 -0
- data/lib/holistic/ruby/reference/find_referenced_scope.rb +18 -0
- data/lib/holistic/ruby/reference/record.rb +13 -0
- data/lib/holistic/ruby/reference/register.rb +15 -0
- data/lib/holistic/ruby/reference/repository.rb +71 -0
- data/lib/holistic/ruby/reference/unregister.rb +11 -0
- data/lib/holistic/ruby/scope/kind.rb +11 -0
- data/lib/holistic/ruby/scope/list_references.rb +32 -0
- data/lib/holistic/ruby/scope/location.rb +43 -0
- data/lib/holistic/ruby/scope/outline.rb +52 -0
- data/lib/holistic/ruby/scope/record.rb +52 -0
- data/lib/holistic/ruby/scope/register.rb +31 -0
- data/lib/holistic/ruby/scope/repository.rb +49 -0
- data/lib/holistic/ruby/scope/unregister.rb +27 -0
- data/lib/holistic/ruby/type_inference/clue/method_call.rb +15 -0
- data/lib/holistic/ruby/type_inference/clue/scope_reference.rb +13 -0
- data/lib/holistic/ruby/type_inference/conclusion.rb +20 -0
- data/lib/holistic/ruby/type_inference/solve.rb +110 -0
- data/lib/holistic/ruby/type_inference/solve_pending_references.rb +13 -0
- data/lib/holistic/version.rb +5 -0
- data/lib/holistic.rb +27 -0
- metadata +158 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Holistic::LanguageServer
|
4
|
+
class Stdio::Parser
|
5
|
+
def initialize
|
6
|
+
@buffer = ::String.new
|
7
|
+
@overflow_from_previous_ingestion = ::String.new
|
8
|
+
@in_header = true
|
9
|
+
@content_length = 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def ingest(payload)
|
13
|
+
payload.each_char do |char|
|
14
|
+
if @in_header || !completed?
|
15
|
+
@buffer.concat(char)
|
16
|
+
else
|
17
|
+
@overflow_from_previous_ingestion.concat(char)
|
18
|
+
end
|
19
|
+
|
20
|
+
if @in_header
|
21
|
+
prepare_to_parse_message! if @buffer.end_with?(Protocol::END_OF_HEADER)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def completed?
|
27
|
+
!@in_header && @content_length == @buffer.length
|
28
|
+
end
|
29
|
+
|
30
|
+
def message
|
31
|
+
Message.new(::JSON.parse(@buffer))
|
32
|
+
end
|
33
|
+
|
34
|
+
def clear
|
35
|
+
left_over = @overflow_from_previous_ingestion.dup
|
36
|
+
@overflow_from_previous_ingestion.clear
|
37
|
+
|
38
|
+
@buffer.clear
|
39
|
+
@in_header = true
|
40
|
+
@content_length = 0
|
41
|
+
|
42
|
+
ingest(left_over) if !left_over.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
MissingContentLengthHeaderError = ::Class.new(::StandardError)
|
48
|
+
|
49
|
+
def prepare_to_parse_message!
|
50
|
+
@buffer.each_line do |line|
|
51
|
+
key, value = line.split(":").map(&:strip)
|
52
|
+
|
53
|
+
if key == Protocol::CONTENT_LENGTH_HEADER
|
54
|
+
@in_header = false
|
55
|
+
@content_length = Integer(value)
|
56
|
+
@buffer.clear
|
57
|
+
|
58
|
+
return
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
raise MissingContentLengthHeaderError, @buffer
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Holistic::LanguageServer::Stdio
|
4
|
+
class Server
|
5
|
+
def initialize(input = STDIN, output = STDOUT)
|
6
|
+
@input = input
|
7
|
+
@output = output
|
8
|
+
@stopped = false
|
9
|
+
@on_data_received = -> { raise ::NotImplementedError }
|
10
|
+
|
11
|
+
set_output_to_binary_mode!
|
12
|
+
end
|
13
|
+
|
14
|
+
def start_input_loop(&block)
|
15
|
+
@on_data_received = block
|
16
|
+
|
17
|
+
read_input until @stopped
|
18
|
+
end
|
19
|
+
|
20
|
+
def write_to_output(payload)
|
21
|
+
@output.write(payload)
|
22
|
+
@output.flush
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop!
|
26
|
+
@stopped = true
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def set_output_to_binary_mode!
|
32
|
+
@output.binmode
|
33
|
+
end
|
34
|
+
|
35
|
+
def read_input
|
36
|
+
begin
|
37
|
+
@input.flush
|
38
|
+
payload = @input.sysread(255)
|
39
|
+
|
40
|
+
@on_data_received.call(payload)
|
41
|
+
rescue ::EOFError, ::IOError, ::Errno::ECONNRESET, ::Errno::ENOTSOCK
|
42
|
+
@stopped = true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Holistic::LanguageServer
|
4
|
+
module Stdio::Start
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def call
|
8
|
+
::Holistic.logger.info("starting holistic-ruby server on dir: #{::Dir.getwd}")
|
9
|
+
|
10
|
+
start_language_server_lifecycle!
|
11
|
+
|
12
|
+
server = Stdio::Server.new
|
13
|
+
parser = Stdio::Parser.new
|
14
|
+
|
15
|
+
server.start_input_loop do |payload|
|
16
|
+
parser.ingest(payload)
|
17
|
+
|
18
|
+
while parser.completed?
|
19
|
+
response = ::Holistic::LanguageServer::Router.dispatch(parser.message)
|
20
|
+
|
21
|
+
case response
|
22
|
+
in Response::Success then server.write_to_output(response.encode)
|
23
|
+
in Response::Error then server.write_to_output(response.encode)
|
24
|
+
in Response::NotFound then nil
|
25
|
+
in Response::Drop then nil
|
26
|
+
in Response::Exit then server.stop!
|
27
|
+
end
|
28
|
+
|
29
|
+
parser.clear
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
::Holistic.logger.info("closing holistic-ruby server on dir #{::Dir.getwd}")
|
34
|
+
rescue ::StandardError => err
|
35
|
+
::Holistic.logger.info("crash from Stdio::Start")
|
36
|
+
::Holistic.logger.info(err.inspect)
|
37
|
+
::Holistic.logger.info(err.backtrace)
|
38
|
+
|
39
|
+
raise err
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def start_language_server_lifecycle!
|
45
|
+
::Holistic::LanguageServer::Current.lifecycle = ::Holistic::LanguageServer::Lifecycle.new
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Holistic::Ruby::Autocompletion
|
4
|
+
module Suggest
|
5
|
+
extend self
|
6
|
+
|
7
|
+
Suggestion = ::Data.define(:code, :kind)
|
8
|
+
|
9
|
+
def call(code:, scope:)
|
10
|
+
lookup_scope = scope
|
11
|
+
|
12
|
+
if code.start_with?("::")
|
13
|
+
lookup_scope = lookup_scope.parent until lookup_scope.root?
|
14
|
+
end
|
15
|
+
|
16
|
+
suggest_namespaces_from_scope(code:, scope: lookup_scope)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
NonMethods = ->(scope) { !scope.method? }
|
22
|
+
|
23
|
+
def suggest_namespaces_from_scope(code:, scope:)
|
24
|
+
suggestions = []
|
25
|
+
|
26
|
+
partial_namespaces = code.split(/(::)/).compact_blank
|
27
|
+
namespace_to_autocomplete = partial_namespaces.pop.then { _1 == "::" ? "" : _1 }
|
28
|
+
namespaces_to_resolve = partial_namespaces.reject { _1 == "::" }
|
29
|
+
|
30
|
+
namespaces_to_resolve.each do |namespace_name|
|
31
|
+
scope = resolve_scope(name: namespace_name, from_scope: scope)
|
32
|
+
|
33
|
+
return suggestions if scope.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
# special case when user did not type :: at the end but the current word
|
37
|
+
# is matches an existing namespace. In this case, suggestions will start with ::.
|
38
|
+
# For example:
|
39
|
+
#
|
40
|
+
# \/ cursor here
|
41
|
+
# typing: "::MyApp::Payments"
|
42
|
+
# suggestions: ["::Record", "::SendReminder"]
|
43
|
+
resolve_scope(name: namespace_to_autocomplete, from_scope: scope)&.then do |fully_typed_scope|
|
44
|
+
scope = fully_typed_scope
|
45
|
+
namespace_to_autocomplete = ""
|
46
|
+
end
|
47
|
+
|
48
|
+
should_search_upwards = namespaces_to_resolve.empty?
|
49
|
+
|
50
|
+
search = ->(scope) do
|
51
|
+
scope.children.filter(&NonMethods).each do |child_scope|
|
52
|
+
if child_scope.name.start_with?(namespace_to_autocomplete)
|
53
|
+
suggestions << Suggestion.new(code: child_scope.name, kind: child_scope.kind)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
search.(scope.parent) if scope.parent.present? && should_search_upwards
|
58
|
+
end
|
59
|
+
|
60
|
+
search.(scope)
|
61
|
+
|
62
|
+
suggestions
|
63
|
+
end
|
64
|
+
|
65
|
+
def resolve_scope(name:, from_scope:)
|
66
|
+
resolved_scope = from_scope.children.find { |scope| scope.name == name }
|
67
|
+
|
68
|
+
if resolved_scope.nil? && from_scope.parent.present?
|
69
|
+
resolved_scope = resolve_scope(name:, from_scope: from_scope.parent)
|
70
|
+
end
|
71
|
+
|
72
|
+
resolved_scope
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Holistic::Ruby::Parser
|
4
|
+
class ConstantResolution
|
5
|
+
attr_reader :scope_repository, :scope
|
6
|
+
|
7
|
+
def initialize(scope_repository:, root_scope:)
|
8
|
+
@scope_repository = scope_repository
|
9
|
+
@scope = root_scope
|
10
|
+
@constant_resolution_possibilities = ["::"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def current
|
14
|
+
@constant_resolution_possibilities.dup
|
15
|
+
end
|
16
|
+
|
17
|
+
def register_child_module(nesting:, location:, &block)
|
18
|
+
starting_scope = @scope
|
19
|
+
|
20
|
+
nesting.each do |name|
|
21
|
+
@scope =
|
22
|
+
::Holistic::Ruby::Scope::Register.call(
|
23
|
+
repository: @scope_repository,
|
24
|
+
parent: @scope,
|
25
|
+
kind: ::Holistic::Ruby::Scope::Kind::MODULE,
|
26
|
+
name:,
|
27
|
+
location:
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
@constant_resolution_possibilities.unshift(@scope.fully_qualified_name)
|
32
|
+
|
33
|
+
block.call
|
34
|
+
|
35
|
+
@scope = starting_scope
|
36
|
+
@constant_resolution_possibilities.shift
|
37
|
+
end
|
38
|
+
|
39
|
+
def register_child_class(nesting:, location:, &block)
|
40
|
+
starting_scope = @scope
|
41
|
+
|
42
|
+
nesting.each do |name|
|
43
|
+
@scope =
|
44
|
+
::Holistic::Ruby::Scope::Register.call(
|
45
|
+
repository: @scope_repository,
|
46
|
+
parent: @scope,
|
47
|
+
kind: ::Holistic::Ruby::Scope::Kind::CLASS,
|
48
|
+
name:,
|
49
|
+
location:
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
@constant_resolution_possibilities.unshift(@scope.fully_qualified_name)
|
54
|
+
|
55
|
+
block.call
|
56
|
+
|
57
|
+
@scope = starting_scope
|
58
|
+
@constant_resolution_possibilities.shift
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Holistic::Ruby::Parser
|
4
|
+
module LiveEditing::ProcessFileChanged
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def call(application:, file:)
|
8
|
+
references_to_recalculate = identify_references_to_recalculate_type_inference(application:, file:)
|
9
|
+
|
10
|
+
unregister_scopes_in_file(application:, file:)
|
11
|
+
unregsiter_references_in_file(application:, file:)
|
12
|
+
|
13
|
+
parse_again(application:, file:)
|
14
|
+
|
15
|
+
recalculate_type_inference_for_references(application:, references: references_to_recalculate)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def identify_references_to_recalculate_type_inference(application:, file:)
|
21
|
+
# we need to reject references declared in the same because they're already going to be
|
22
|
+
# unregistered and reparsed. If we don't do that, we'll end up with duplicated reference records.
|
23
|
+
|
24
|
+
application.references
|
25
|
+
.list_references_to_scopes_in_file(scopes: application.scopes, file_path: file.path)
|
26
|
+
.reject { _1.location.file_path == file.path }
|
27
|
+
end
|
28
|
+
|
29
|
+
def unregister_scopes_in_file(application:, file:)
|
30
|
+
application.scopes.list_scopes_in_file(file.path).each do |scope|
|
31
|
+
::Holistic::Ruby::Scope::Unregister.call(
|
32
|
+
repository: application.scopes,
|
33
|
+
fully_qualified_name: scope.fully_qualified_name,
|
34
|
+
file_path: file.path
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def unregsiter_references_in_file(application:, file:)
|
40
|
+
application.references.list_references_in_file(file.path).each do |reference|
|
41
|
+
::Holistic::Ruby::Reference::Unregister.call(
|
42
|
+
repository: application.references,
|
43
|
+
reference: reference
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse_again(application:, file:)
|
49
|
+
ParseFile.call(application:, file:)
|
50
|
+
|
51
|
+
::Holistic::Ruby::TypeInference::SolvePendingReferences.call(application:)
|
52
|
+
end
|
53
|
+
|
54
|
+
def recalculate_type_inference_for_references(application:, references:)
|
55
|
+
references.each do |reference|
|
56
|
+
reference.conclusion = ::Holistic::Ruby::TypeInference::Conclusion.pending
|
57
|
+
|
58
|
+
::Holistic::Ruby::TypeInference::Solve.call(application:, reference:)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Holistic::Ruby::Parser
|
4
|
+
class NestingSyntax
|
5
|
+
attr_reader :value, :is_root_scope
|
6
|
+
|
7
|
+
def self.build(node)
|
8
|
+
nesting_syntax = NestingSyntax.new
|
9
|
+
|
10
|
+
append = ->(node) do
|
11
|
+
case node
|
12
|
+
when ::SyntaxTree::ConstRef then nesting_syntax << node.child_nodes[0].value
|
13
|
+
when ::SyntaxTree::VarField then nesting_syntax << node.child_nodes.first.value
|
14
|
+
when ::SyntaxTree::Const then nesting_syntax << node.value
|
15
|
+
when ::SyntaxTree::VCall then append.(node.child_nodes.first)
|
16
|
+
when ::SyntaxTree::Ident then nesting_syntax << node.value
|
17
|
+
when ::SyntaxTree::IVar then nesting_syntax << node.value
|
18
|
+
when ::SyntaxTree::Period then nesting_syntax << "."
|
19
|
+
when ::SyntaxTree::Paren then append.(node.child_nodes[1]) # node.child_nodes[0] is ::SyntaxTree::LParen
|
20
|
+
when ::SyntaxTree::ARef then append.(node.child_nodes.first) # not sure what to do here e.g. `ActiveRecord::Migration[7.0]`
|
21
|
+
when ::SyntaxTree::CallNode then node.child_nodes.each(&append)
|
22
|
+
when ::SyntaxTree::IfOp then nesting_syntax << "[conditional]" # not sure what tod o here
|
23
|
+
when ::SyntaxTree::VarRef then node.child_nodes.each(&append)
|
24
|
+
when ::SyntaxTree::ConstPathRef then node.child_nodes.each(&append)
|
25
|
+
when ::SyntaxTree::Statements then node.child_nodes.each(&append)
|
26
|
+
when ::SyntaxTree::TopConstRef then nesting_syntax.mark_as_root_scope! and node.child_nodes.each(&append)
|
27
|
+
# else pp(original_node) and pp(Current.file.path) and raise "Unexpected node type: #{node.class}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
append.(node)
|
32
|
+
|
33
|
+
nesting_syntax
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(value = [])
|
37
|
+
@value = Array(value)
|
38
|
+
@is_root_scope = false
|
39
|
+
end
|
40
|
+
|
41
|
+
def mark_as_root_scope!
|
42
|
+
@is_root_scope = true
|
43
|
+
end
|
44
|
+
|
45
|
+
def supported?
|
46
|
+
@value.any?
|
47
|
+
end
|
48
|
+
|
49
|
+
def unsupported?
|
50
|
+
!supported?
|
51
|
+
end
|
52
|
+
|
53
|
+
def root_scope_resolution?
|
54
|
+
is_root_scope
|
55
|
+
end
|
56
|
+
|
57
|
+
def constant?
|
58
|
+
return false if unsupported?
|
59
|
+
|
60
|
+
@value.last[0].then { _1 == _1.upcase }
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
@value.join("::")
|
65
|
+
end
|
66
|
+
|
67
|
+
def eql?(other)
|
68
|
+
other.class == self.class && other.to_s == to_s
|
69
|
+
end
|
70
|
+
|
71
|
+
alias == eql?
|
72
|
+
|
73
|
+
delegate :each, to: :value
|
74
|
+
delegate :<<, to: :value
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Holistic::Ruby::Parser
|
4
|
+
class ProgramVisitor < ::SyntaxTree::Visitor
|
5
|
+
attr_reader :application, :file
|
6
|
+
|
7
|
+
def initialize(application:, constant_resolution:, file:)
|
8
|
+
@application = application
|
9
|
+
@constant_resolution = constant_resolution
|
10
|
+
@file = file
|
11
|
+
|
12
|
+
super()
|
13
|
+
end
|
14
|
+
|
15
|
+
visit_methods do
|
16
|
+
def visit_module(node)
|
17
|
+
declaration_node, body_node = node.child_nodes
|
18
|
+
|
19
|
+
nesting = NestingSyntax.build(declaration_node)
|
20
|
+
location = build_scope_location(declaration_node:, body_node:)
|
21
|
+
|
22
|
+
@constant_resolution.register_child_module(nesting:, location:) do
|
23
|
+
visit(body_node)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def visit_class(node)
|
28
|
+
declaration_node, superclass_node, body_node = node.child_nodes
|
29
|
+
|
30
|
+
if superclass_node
|
31
|
+
register_reference(nesting: NestingSyntax.build(superclass_node), location: build_location(superclass_node))
|
32
|
+
end
|
33
|
+
|
34
|
+
nesting = NestingSyntax.build(declaration_node)
|
35
|
+
location = build_scope_location(declaration_node:, body_node:)
|
36
|
+
|
37
|
+
@constant_resolution.register_child_class(nesting:, location:) do
|
38
|
+
visit(body_node)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def visit_def(node)
|
43
|
+
instance_node, period_node, method_name_node, _params, body_node = node.child_nodes
|
44
|
+
|
45
|
+
method_name =
|
46
|
+
if instance_node.present? && period_node.present?
|
47
|
+
instance_node.child_nodes.first.value + period_node.value + method_name_node.value
|
48
|
+
else
|
49
|
+
method_name_node.value
|
50
|
+
end
|
51
|
+
|
52
|
+
location = build_scope_location(declaration_node: method_name_node, body_node:)
|
53
|
+
|
54
|
+
::Holistic::Ruby::Scope::Register.call(
|
55
|
+
repository: @application.scopes,
|
56
|
+
parent: @constant_resolution.scope,
|
57
|
+
kind: ::Holistic::Ruby::Scope::Kind::METHOD,
|
58
|
+
name: method_name,
|
59
|
+
location:
|
60
|
+
)
|
61
|
+
|
62
|
+
visit(body_node)
|
63
|
+
end
|
64
|
+
|
65
|
+
def visit_vcall(node)
|
66
|
+
method_call_clue = ::Holistic::Ruby::TypeInference::Clue::MethodCall.new(
|
67
|
+
nesting: nil,
|
68
|
+
method_name: node.child_nodes.first.value,
|
69
|
+
resolution_possibilities: @constant_resolution.current
|
70
|
+
)
|
71
|
+
|
72
|
+
::Holistic::Ruby::Reference::Register.call(
|
73
|
+
repository: @application.references,
|
74
|
+
scope: @constant_resolution.scope,
|
75
|
+
clues: [method_call_clue],
|
76
|
+
location: build_location(node)
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def visit_call(node)
|
81
|
+
instance_node, _period_node, method_name_node, arguments_nodes = node.child_nodes
|
82
|
+
|
83
|
+
visit(instance_node)
|
84
|
+
visit(arguments_nodes)
|
85
|
+
|
86
|
+
nesting =
|
87
|
+
if instance_node.is_a?(::SyntaxTree::CallNode)
|
88
|
+
NestingSyntax.build(instance_node.child_nodes[2])
|
89
|
+
elsif instance_node.present?
|
90
|
+
NestingSyntax.build(instance_node)
|
91
|
+
else
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
return if nesting.present? && nesting.unsupported?
|
96
|
+
|
97
|
+
# method_name_node is nil for the syntax `DoSomething.(value)`
|
98
|
+
method_name = method_name_node.nil? ? "call" : method_name_node.value
|
99
|
+
|
100
|
+
method_call_clue = ::Holistic::Ruby::TypeInference::Clue::MethodCall.new(
|
101
|
+
nesting:,
|
102
|
+
method_name:,
|
103
|
+
resolution_possibilities: @constant_resolution.current
|
104
|
+
)
|
105
|
+
|
106
|
+
::Holistic::Ruby::Reference::Register.call(
|
107
|
+
repository: @application.references,
|
108
|
+
scope: @constant_resolution.scope,
|
109
|
+
clues: [method_call_clue],
|
110
|
+
location: build_location(method_name_node || instance_node)
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
def visit_assign(node)
|
115
|
+
assign_node, body_node = node.child_nodes
|
116
|
+
|
117
|
+
if !assign_node.child_nodes.first.is_a?(::SyntaxTree::Const)
|
118
|
+
visit(body_node)
|
119
|
+
|
120
|
+
return # TODO
|
121
|
+
end
|
122
|
+
|
123
|
+
# Are we assigning to something that opens a block? If so, we need to register the child scope
|
124
|
+
# and visit the statements. This is needed to support methods defined in `Data.define` and `Struct.new`.
|
125
|
+
if body_node.is_a?(::SyntaxTree::MethodAddBlock)
|
126
|
+
call_node, block_node = body_node.child_nodes
|
127
|
+
|
128
|
+
nesting = NestingSyntax.build(assign_node)
|
129
|
+
location = build_scope_location(declaration_node: assign_node, body_node: block_node)
|
130
|
+
|
131
|
+
@constant_resolution.register_child_class(nesting:, location:) do
|
132
|
+
visit(block_node)
|
133
|
+
end
|
134
|
+
|
135
|
+
return
|
136
|
+
end
|
137
|
+
|
138
|
+
location = build_scope_location(declaration_node: assign_node, body_node:)
|
139
|
+
|
140
|
+
::Holistic::Ruby::Scope::Register.call(
|
141
|
+
repository: @application.scopes,
|
142
|
+
parent: @constant_resolution.scope,
|
143
|
+
kind: ::Holistic::Ruby::Scope::Kind::LAMBDA,
|
144
|
+
name: assign_node.child_nodes.first.value,
|
145
|
+
location:
|
146
|
+
)
|
147
|
+
|
148
|
+
visit(body_node)
|
149
|
+
end
|
150
|
+
|
151
|
+
def visit_const_path_ref(node)
|
152
|
+
register_reference(nesting: NestingSyntax.build(node), location: build_location(node))
|
153
|
+
end
|
154
|
+
|
155
|
+
def visit_top_const_ref(node)
|
156
|
+
register_reference(nesting: NestingSyntax.build(node), location: build_location(node))
|
157
|
+
end
|
158
|
+
|
159
|
+
def visit_const(node)
|
160
|
+
register_reference(nesting: NestingSyntax.build(node), location: build_location(node))
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def register_reference(nesting:, location:)
|
167
|
+
clue = ::Holistic::Ruby::TypeInference::Clue::ScopeReference.new(
|
168
|
+
nesting:,
|
169
|
+
resolution_possibilities: @constant_resolution.current
|
170
|
+
)
|
171
|
+
|
172
|
+
::Holistic::Ruby::Reference::Register.call(
|
173
|
+
repository: @application.references,
|
174
|
+
scope: @constant_resolution.scope,
|
175
|
+
clues: [clue],
|
176
|
+
location:
|
177
|
+
)
|
178
|
+
end
|
179
|
+
|
180
|
+
def build_scope_location(declaration_node:, body_node:)
|
181
|
+
::Holistic::Ruby::Scope::Location.new(
|
182
|
+
declaration: build_location(declaration_node),
|
183
|
+
body: build_location(body_node)
|
184
|
+
)
|
185
|
+
end
|
186
|
+
|
187
|
+
def build_location(node)
|
188
|
+
# https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position
|
189
|
+
offset_to_match_language_server_zero_based_position = 1
|
190
|
+
|
191
|
+
start_line = node.location.start_line - offset_to_match_language_server_zero_based_position
|
192
|
+
end_line = node.location.end_line - offset_to_match_language_server_zero_based_position
|
193
|
+
start_column = node.location.start_column
|
194
|
+
end_column = node.location.end_column
|
195
|
+
|
196
|
+
# syntax_tree seems to have a bug with the bodystmt node.
|
197
|
+
# It sets the end_column lower than the start_column.
|
198
|
+
if start_line == end_line && start_column > end_column
|
199
|
+
start_column, end_column = end_column, start_column
|
200
|
+
end
|
201
|
+
|
202
|
+
::Holistic::Document::Location.new(file_path: file.path, start_line:, start_column:, end_line:, end_column:)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Holistic::Ruby::Parser
|
4
|
+
# TODO: move to an attibute of the scope
|
5
|
+
class TableOfContents
|
6
|
+
attr_reader :records
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@records = Hash.new { |hash, key| hash[key] = {} }
|
10
|
+
end
|
11
|
+
|
12
|
+
def register(scope:, name:, clue:)
|
13
|
+
@records[scope.fully_qualified_name][name] ||= []
|
14
|
+
@records[scope.fully_qualified_name][name] << clue
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Holistic::Ruby::Parser
|
4
|
+
ParseFile = ->(application:, file:) do
|
5
|
+
program = ::SyntaxTree.parse(file.read)
|
6
|
+
|
7
|
+
constant_resolution = ConstantResolution.new(
|
8
|
+
scope_repository: application.scopes,
|
9
|
+
root_scope: application.root_scope
|
10
|
+
)
|
11
|
+
|
12
|
+
visitor = ProgramVisitor.new(application:, constant_resolution:, file:)
|
13
|
+
|
14
|
+
visitor.visit(program)
|
15
|
+
rescue ::SyntaxTree::Parser::ParseError
|
16
|
+
::Holistic.logger.info("syntax error on file #{file.path}")
|
17
|
+
end
|
18
|
+
|
19
|
+
ParseDirectory = ->(application:, directory_path:) do
|
20
|
+
::Dir.glob("#{directory_path}/**/*.rb").map do |file_path|
|
21
|
+
file = ::Holistic::Document::File.new(path: file_path)
|
22
|
+
|
23
|
+
ParseFile[application:, file:]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|