holistic-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.standard.yml +3 -0
  4. data/Gemfile +10 -0
  5. data/Gemfile.lock +52 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +35 -0
  8. data/Rakefile +8 -0
  9. data/config/logging.rb +6 -0
  10. data/exe/holistic-ruby +6 -0
  11. data/holistic-ruby.gemspec +34 -0
  12. data/lib/holistic/application.rb +29 -0
  13. data/lib/holistic/background_process.rb +11 -0
  14. data/lib/holistic/database/table.rb +78 -0
  15. data/lib/holistic/document/cursor.rb +9 -0
  16. data/lib/holistic/document/file.rb +36 -0
  17. data/lib/holistic/document/location.rb +35 -0
  18. data/lib/holistic/document/unsaved/change.rb +24 -0
  19. data/lib/holistic/document/unsaved/collection.rb +21 -0
  20. data/lib/holistic/document/unsaved/record.rb +83 -0
  21. data/lib/holistic/extensions/events.rb +37 -0
  22. data/lib/holistic/extensions/ruby/stdlib.rb +43 -0
  23. data/lib/holistic/language_server/current.rb +11 -0
  24. data/lib/holistic/language_server/format/file_uri.rb +19 -0
  25. data/lib/holistic/language_server/lifecycle.rb +59 -0
  26. data/lib/holistic/language_server/message.rb +21 -0
  27. data/lib/holistic/language_server/protocol.rb +45 -0
  28. data/lib/holistic/language_server/request.rb +21 -0
  29. data/lib/holistic/language_server/requests/lifecycle/exit.rb +10 -0
  30. data/lib/holistic/language_server/requests/lifecycle/initialize.rb +75 -0
  31. data/lib/holistic/language_server/requests/lifecycle/initialized.rb +13 -0
  32. data/lib/holistic/language_server/requests/lifecycle/shutdown.rb +14 -0
  33. data/lib/holistic/language_server/requests/text_document/completion.rb +68 -0
  34. data/lib/holistic/language_server/requests/text_document/did_change.rb +30 -0
  35. data/lib/holistic/language_server/requests/text_document/did_close.rb +33 -0
  36. data/lib/holistic/language_server/requests/text_document/did_open.rb +16 -0
  37. data/lib/holistic/language_server/requests/text_document/did_save.rb +33 -0
  38. data/lib/holistic/language_server/requests/text_document/find_references.rb +52 -0
  39. data/lib/holistic/language_server/requests/text_document/go_to_definition.rb +64 -0
  40. data/lib/holistic/language_server/response.rb +39 -0
  41. data/lib/holistic/language_server/router.rb +48 -0
  42. data/lib/holistic/language_server/stdio/parser.rb +65 -0
  43. data/lib/holistic/language_server/stdio/server.rb +46 -0
  44. data/lib/holistic/language_server/stdio/start.rb +48 -0
  45. data/lib/holistic/ruby/autocompletion/suggest.rb +75 -0
  46. data/lib/holistic/ruby/parser/constant_resolution.rb +61 -0
  47. data/lib/holistic/ruby/parser/live_editing/process_file_changed.rb +62 -0
  48. data/lib/holistic/ruby/parser/nesting_syntax.rb +76 -0
  49. data/lib/holistic/ruby/parser/program_visitor.rb +205 -0
  50. data/lib/holistic/ruby/parser/table_of_contents.rb +17 -0
  51. data/lib/holistic/ruby/parser.rb +26 -0
  52. data/lib/holistic/ruby/reference/find_referenced_scope.rb +18 -0
  53. data/lib/holistic/ruby/reference/record.rb +13 -0
  54. data/lib/holistic/ruby/reference/register.rb +15 -0
  55. data/lib/holistic/ruby/reference/repository.rb +71 -0
  56. data/lib/holistic/ruby/reference/unregister.rb +11 -0
  57. data/lib/holistic/ruby/scope/kind.rb +11 -0
  58. data/lib/holistic/ruby/scope/list_references.rb +32 -0
  59. data/lib/holistic/ruby/scope/location.rb +43 -0
  60. data/lib/holistic/ruby/scope/outline.rb +52 -0
  61. data/lib/holistic/ruby/scope/record.rb +52 -0
  62. data/lib/holistic/ruby/scope/register.rb +31 -0
  63. data/lib/holistic/ruby/scope/repository.rb +49 -0
  64. data/lib/holistic/ruby/scope/unregister.rb +27 -0
  65. data/lib/holistic/ruby/type_inference/clue/method_call.rb +15 -0
  66. data/lib/holistic/ruby/type_inference/clue/scope_reference.rb +13 -0
  67. data/lib/holistic/ruby/type_inference/conclusion.rb +20 -0
  68. data/lib/holistic/ruby/type_inference/solve.rb +110 -0
  69. data/lib/holistic/ruby/type_inference/solve_pending_references.rb +13 -0
  70. data/lib/holistic/version.rb +5 -0
  71. data/lib/holistic.rb +27 -0
  72. 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