holistic-ruby 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|