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.
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