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,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::Reference
|
|
4
|
+
module FindReferencedScope
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def call(application:, cursor:)
|
|
8
|
+
reference = application.references.find_by_cursor(cursor)
|
|
9
|
+
|
|
10
|
+
return :not_found if reference.nil?
|
|
11
|
+
return :could_not_find_referenced_scope if reference.conclusion.dependency_identifier.nil?
|
|
12
|
+
|
|
13
|
+
referenced_scope = application.scopes.find_by_fully_qualified_name(reference.conclusion.dependency_identifier)
|
|
14
|
+
|
|
15
|
+
[:referenced_scope_found, {reference:, referenced_scope:}]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::Reference
|
|
4
|
+
module Register
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def call(repository:, scope:, clues:, location:)
|
|
8
|
+
conclusion = ::Holistic::Ruby::TypeInference::Conclusion.pending
|
|
9
|
+
|
|
10
|
+
reference = ::Holistic::Ruby::Reference::Record.new(scope:, clues:, location:, conclusion:)
|
|
11
|
+
|
|
12
|
+
repository.register_reference(reference)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::Reference
|
|
4
|
+
class Repository
|
|
5
|
+
attr_reader :table
|
|
6
|
+
|
|
7
|
+
INDICES = [
|
|
8
|
+
:file_path,
|
|
9
|
+
:type_inference_status,
|
|
10
|
+
:referenced_scope_fully_qualified_name
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@table = ::Holistic::Database::Table.new(primary_attribute: :identifier, indices: INDICES)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def register_reference(reference)
|
|
18
|
+
table.update({
|
|
19
|
+
reference:,
|
|
20
|
+
identifier: reference.identifier,
|
|
21
|
+
file_path: reference.location.file_path,
|
|
22
|
+
type_inference_status: reference.conclusion.status,
|
|
23
|
+
referenced_scope_fully_qualified_name: reference.conclusion.dependency_identifier
|
|
24
|
+
})
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def find_by_cursor(cursor)
|
|
28
|
+
table.filter(:file_path, cursor.file_path).map { _1[:reference] }.each do |reference|
|
|
29
|
+
return reference if reference.location.contains?(cursor)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def list_references_to(fully_qualified_scope_name)
|
|
36
|
+
table.filter(:referenced_scope_fully_qualified_name, fully_qualified_scope_name).map { _1[:reference] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def list_references_in_file(file_path)
|
|
40
|
+
table.filter(:file_path, file_path).map { _1[:reference] }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def list_references_to_scopes_in_file(scopes:, file_path:)
|
|
44
|
+
scopes.list_scopes_in_file(file_path).flat_map do |scope|
|
|
45
|
+
table.filter(:referenced_scope_fully_qualified_name, scope.fully_qualified_name).map { _1[:reference] }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def list_references_pending_type_inference_conclusion
|
|
50
|
+
table.filter(:type_inference_status, ::Holistic::Ruby::TypeInference::STATUS_PENDING).map { _1[:reference] }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def delete(identifier)
|
|
54
|
+
table.delete(identifier)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
concerning :TestHelpers do
|
|
58
|
+
def find_reference_to(scope_name)
|
|
59
|
+
table.all.map { _1[:reference] }.find do |reference|
|
|
60
|
+
reference.conclusion.dependency_identifier == scope_name || reference.clues.find { _1.to_s == scope_name }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def find_by_code_content(code_content)
|
|
65
|
+
table.all.map { _1[:reference] }.find do |reference|
|
|
66
|
+
reference.clues.find { _1.to_s == code_content }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::Scope
|
|
4
|
+
module ListReferences
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
QueryReferencesRecursively = ->(application, scope) do
|
|
8
|
+
references_to_scope = application.references.list_references_to(scope.fully_qualified_name)
|
|
9
|
+
|
|
10
|
+
references_to_child_scopes = scope.children.flat_map { QueryReferencesRecursively.call(application, _1) }
|
|
11
|
+
|
|
12
|
+
references_to_scope + references_to_child_scopes
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Relevance = ->(reference) do
|
|
16
|
+
# TODO: should the location answer the kind of file it is? application code, config, spec, etc. Not sure.
|
|
17
|
+
looks_like_a_spec = reference.location.file_path.include?("_spec.rb") || reference.location.file_path.include?("_test.rb")
|
|
18
|
+
|
|
19
|
+
looks_like_a_spec ? 1 : 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(application:, cursor:)
|
|
23
|
+
scope = application.scopes.find_by_cursor(cursor)
|
|
24
|
+
|
|
25
|
+
return :not_found if scope.nil?
|
|
26
|
+
|
|
27
|
+
references = QueryReferencesRecursively.call(application, scope).sort_by(&Relevance)
|
|
28
|
+
|
|
29
|
+
[:references_listed, {references:}]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::Scope
|
|
4
|
+
class Location
|
|
5
|
+
class Collection
|
|
6
|
+
attr_reader :scope, :items
|
|
7
|
+
|
|
8
|
+
def initialize(scope, location)
|
|
9
|
+
raise ::ArgumentError if location.present? && !location.is_a?(Location)
|
|
10
|
+
|
|
11
|
+
@scope = scope
|
|
12
|
+
@items = location.nil? ? [] : [location]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def main
|
|
16
|
+
location_matching_scope_name || items.first
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
delegate :<<, to: :items
|
|
20
|
+
delegate :each, to: :items
|
|
21
|
+
delegate :map, to: :items
|
|
22
|
+
delegate :reject!, to: :items
|
|
23
|
+
delegate :any?, to: :items
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def location_matching_scope_name
|
|
28
|
+
scope_name_in_snake_case = scope.name.underscore
|
|
29
|
+
|
|
30
|
+
items.find do |location|
|
|
31
|
+
::File.basename(location.declaration.file_path) == "#{scope_name_in_snake_case}.rb"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :declaration, :body
|
|
37
|
+
|
|
38
|
+
def initialize(declaration:, body:)
|
|
39
|
+
@declaration = declaration
|
|
40
|
+
@body = body
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::Scope
|
|
4
|
+
module Outline
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
Result = ::Struct.new(
|
|
8
|
+
:declarations,
|
|
9
|
+
:dependencies,
|
|
10
|
+
:references,
|
|
11
|
+
:dependants,
|
|
12
|
+
keyword_init: true
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
QueryChildScopesRecursively = ->(application, scope) do
|
|
16
|
+
scope.children + scope.children.flat_map { QueryChildScopesRecursively[application, _1] }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
QueryDependenciesRecursively = ->(application, outlined_scope, scope) do
|
|
20
|
+
is_local_dependency = ->(reference) do
|
|
21
|
+
scope = application.scopes.find_by_fully_qualified_name(reference.conclusion.dependency_identifier)
|
|
22
|
+
|
|
23
|
+
scope.eql?(outlined_scope) || scope.descendant?(outlined_scope)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
scope.locations.each do |scope_location|
|
|
29
|
+
application.references
|
|
30
|
+
.list_references_in_file(scope_location.declaration.file_path)
|
|
31
|
+
.filter { |reference| reference.scope == scope }
|
|
32
|
+
.filter { |reference| reference.conclusion.dependency_identifier.present? }
|
|
33
|
+
.reject(&is_local_dependency)
|
|
34
|
+
.tap { dependencies.concat(_1) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
scope.children.map(&QueryDependenciesRecursively.curry[application, outlined_scope]).flatten.concat(dependencies)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call(application:, scope:)
|
|
41
|
+
declarations = QueryChildScopesRecursively.call(application, scope).sort_by { _1.fully_qualified_name }
|
|
42
|
+
|
|
43
|
+
dependencies = QueryDependenciesRecursively.call(application, scope, scope).uniq { _1.conclusion.dependency_identifier }
|
|
44
|
+
|
|
45
|
+
references = application.references.list_references_to(scope.fully_qualified_name)
|
|
46
|
+
|
|
47
|
+
dependants = references.map { |reference| reference.scope }.uniq
|
|
48
|
+
|
|
49
|
+
Result.new(declarations:, dependencies:, references:, dependants:)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::Scope
|
|
4
|
+
class Record
|
|
5
|
+
attr_reader :kind, :name, :parent, :children, :locations
|
|
6
|
+
|
|
7
|
+
def initialize(kind:, name:, parent:, location: nil)
|
|
8
|
+
@kind = kind
|
|
9
|
+
@name = name
|
|
10
|
+
@parent = parent
|
|
11
|
+
@locations = Location::Collection.new(self, location)
|
|
12
|
+
@children = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fully_qualified_name
|
|
16
|
+
return "" if root?
|
|
17
|
+
|
|
18
|
+
separator =
|
|
19
|
+
if kind == Kind::METHOD
|
|
20
|
+
"#"
|
|
21
|
+
else
|
|
22
|
+
"::"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
"#{parent.fully_qualified_name}#{separator}#{name}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def root?
|
|
29
|
+
parent.nil?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def lambda?
|
|
33
|
+
kind == Kind::LAMBDA
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def class?
|
|
37
|
+
kind == Kind::CLASS
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def module?
|
|
41
|
+
kind == Kind::MODULE
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def method?
|
|
45
|
+
kind == Kind::METHOD
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def descendant?(other)
|
|
49
|
+
parent.present? && (parent == other || parent.descendant?(other))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::Scope
|
|
4
|
+
module Register
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def call(repository:, parent:, kind:, name:, location:)
|
|
8
|
+
child_scope = append_location_to_existing_scope(scope: parent, name:, location:) || add_new_scope(parent:, kind:, name:, location:)
|
|
9
|
+
|
|
10
|
+
repository.register_scope(child_scope)
|
|
11
|
+
|
|
12
|
+
child_scope
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def append_location_to_existing_scope(scope:, name:, location:)
|
|
18
|
+
child_scope = scope.children.find { _1.name == name }
|
|
19
|
+
|
|
20
|
+
return if child_scope.nil?
|
|
21
|
+
|
|
22
|
+
child_scope.tap { _1.locations << location }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_new_scope(parent:, kind:, name:, location:)
|
|
26
|
+
child_scope = Record.new(kind:, name:, parent:, location:)
|
|
27
|
+
|
|
28
|
+
child_scope.tap { parent.children << _1 }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::Scope
|
|
4
|
+
class Repository
|
|
5
|
+
attr_reader :table
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@table = ::Holistic::Database::Table.new(primary_attribute: :fully_qualified_name, indices: [:file_paths])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def register_scope(scope)
|
|
12
|
+
table.update({
|
|
13
|
+
fully_qualified_name: scope.fully_qualified_name,
|
|
14
|
+
file_paths: scope.locations.map { |scope_location| scope_location.declaration.file_path },
|
|
15
|
+
scope:
|
|
16
|
+
})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def find_by_fully_qualified_name(fully_qualified_name)
|
|
20
|
+
table.find(fully_qualified_name).try(:dig, :scope)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def find_by_cursor(cursor)
|
|
24
|
+
table.filter(:file_paths, cursor.file_path).map { _1[:scope] }.each do |scope|
|
|
25
|
+
return scope if scope.locations.any? { _1.declaration.contains?(cursor) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find_inner_most_scope_by_cursor(cursor)
|
|
32
|
+
scopes = table.filter(:file_paths, cursor.file_path).map { _1[:scope] }
|
|
33
|
+
|
|
34
|
+
matching_scopes = scopes.filter do |scope|
|
|
35
|
+
scope.locations.any? { _1.body.contains?(cursor) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
matching_scopes.last
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def delete_by_fully_qualified_name(fully_qualified_name)
|
|
42
|
+
table.delete(fully_qualified_name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def list_scopes_in_file(file_path)
|
|
46
|
+
table.filter(:file_paths, file_path).map { _1[:scope] }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::Scope
|
|
4
|
+
module Unregister
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def call(repository:, fully_qualified_name:, file_path:)
|
|
8
|
+
scope = repository.find_by_fully_qualified_name(fully_qualified_name)
|
|
9
|
+
|
|
10
|
+
return :scope_not_found if scope.nil?
|
|
11
|
+
|
|
12
|
+
updated_locations = scope.locations.reject! { |scope_location| scope_location.declaration.file_path == file_path }
|
|
13
|
+
|
|
14
|
+
return :scope_not_defined_in_speciefied_file if updated_locations.nil?
|
|
15
|
+
|
|
16
|
+
if updated_locations.empty?
|
|
17
|
+
scope.parent.children.delete(scope)
|
|
18
|
+
|
|
19
|
+
repository.delete_by_fully_qualified_name(fully_qualified_name)
|
|
20
|
+
else
|
|
21
|
+
repository.register_scope(scope)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
:definition_unregistered
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::TypeInference::Clue
|
|
4
|
+
MethodCall = ::Data.define(
|
|
5
|
+
:nesting,
|
|
6
|
+
:method_name,
|
|
7
|
+
:resolution_possibilities
|
|
8
|
+
) do
|
|
9
|
+
def to_s
|
|
10
|
+
return method_name if nesting.nil?
|
|
11
|
+
|
|
12
|
+
"#{nesting}.#{method_name}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::TypeInference
|
|
4
|
+
STATUS_PENDING = :pending
|
|
5
|
+
STATUS_DONE = :done
|
|
6
|
+
|
|
7
|
+
Conclusion = ::Data.define(:status, :dependency_identifier) do
|
|
8
|
+
def self.pending
|
|
9
|
+
new(status: STATUS_PENDING, dependency_identifier: nil)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.unresolved
|
|
13
|
+
new(status: STATUS_DONE, dependency_identifier: nil)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.done(dependency_identifier)
|
|
17
|
+
new(status: STATUS_DONE, dependency_identifier:)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::TypeInference
|
|
4
|
+
module Solve
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def call(application:, reference:)
|
|
8
|
+
reference.conclusion =
|
|
9
|
+
solve_scope_reference(application:, reference:) ||
|
|
10
|
+
solve_method_call(application:, reference:) ||
|
|
11
|
+
Conclusion.unresolved
|
|
12
|
+
|
|
13
|
+
application.references.register_reference(reference)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def solve_scope_reference(application:, reference:)
|
|
19
|
+
has_scope_reference_clue =
|
|
20
|
+
reference.clues.one? && reference.clues.first.is_a?(Clue::ScopeReference)
|
|
21
|
+
|
|
22
|
+
return unless has_scope_reference_clue
|
|
23
|
+
|
|
24
|
+
scope_reference_clue = reference.clues.first
|
|
25
|
+
|
|
26
|
+
referenced_scope = resolve_scope(
|
|
27
|
+
application:,
|
|
28
|
+
nesting: scope_reference_clue.nesting,
|
|
29
|
+
resolution_possibilities: scope_reference_clue.resolution_possibilities
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if referenced_scope.present?
|
|
33
|
+
return Conclusion.done(referenced_scope.fully_qualified_name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
SolveMethodCallInCurrentScope = ->(application:, reference:, method_call_clue:) do
|
|
40
|
+
referenced_method = resolve_method(application:, scope: reference.scope, method_name: method_call_clue.method_name)
|
|
41
|
+
|
|
42
|
+
return if referenced_method.nil?
|
|
43
|
+
|
|
44
|
+
Conclusion.done(referenced_method.fully_qualified_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
SolveMethodCallInSpecifiedScope = ->(application:, reference:, method_call_clue:) do
|
|
48
|
+
referenced_scope = resolve_scope(
|
|
49
|
+
application:,
|
|
50
|
+
nesting: method_call_clue.nesting,
|
|
51
|
+
resolution_possibilities: method_call_clue.resolution_possibilities
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return if referenced_scope.nil?
|
|
55
|
+
|
|
56
|
+
referenced_method = resolve_method(application:, scope: referenced_scope, method_name: method_call_clue.method_name)
|
|
57
|
+
referenced_method ||= application.extensions.dispatch(:resolve_method_call_known_scope, { reference:, referenced_scope:, method_call_clue: })
|
|
58
|
+
|
|
59
|
+
Conclusion.done(referenced_method.fully_qualified_name) if referenced_method.present?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
SolveMethodCallInLocalVariable = ->(application:, reference:, method_call_clue:) do
|
|
63
|
+
# local_variable_name = method_call_clue.nesting.to_s
|
|
64
|
+
# referenced_scope = guess_scope_for_local_variable(scope: reference.scope, name: local_variable_name)
|
|
65
|
+
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def solve_method_call(application:, reference:)
|
|
70
|
+
has_method_call_clue = reference.clues.one? && reference.clues.first.is_a?(Clue::MethodCall)
|
|
71
|
+
|
|
72
|
+
return unless has_method_call_clue
|
|
73
|
+
|
|
74
|
+
method_call_clue = reference.clues.first
|
|
75
|
+
|
|
76
|
+
if method_call_clue.nesting.nil?
|
|
77
|
+
SolveMethodCallInCurrentScope.call(application:, reference:, method_call_clue:)
|
|
78
|
+
elsif method_call_clue.nesting.constant?
|
|
79
|
+
SolveMethodCallInSpecifiedScope.call(application:, reference:, method_call_clue:)
|
|
80
|
+
else
|
|
81
|
+
SolveMethodCallInLocalVariable.call(application:, reference:, method_call_clue:)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def resolve_scope(application:, nesting:, resolution_possibilities:)
|
|
86
|
+
resolution_possibilities = ["::"] if nesting.root_scope_resolution?
|
|
87
|
+
|
|
88
|
+
resolution_possibilities.each do |resolution_candidate|
|
|
89
|
+
fully_qualified_scope_name =
|
|
90
|
+
if resolution_candidate == "::"
|
|
91
|
+
"::#{nesting.to_s}"
|
|
92
|
+
else
|
|
93
|
+
"#{resolution_candidate}::#{nesting.to_s}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
scope = application.scopes.find_by_fully_qualified_name(fully_qualified_scope_name)
|
|
97
|
+
|
|
98
|
+
return scope if scope.present?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def resolve_method(application:, scope:, method_name:)
|
|
105
|
+
method_fully_qualified_name = "#{scope.fully_qualified_name}##{method_name}"
|
|
106
|
+
|
|
107
|
+
application.scopes.find_by_fully_qualified_name(method_fully_qualified_name)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holistic::Ruby::TypeInference
|
|
4
|
+
module SolvePendingReferences
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def call(application:)
|
|
8
|
+
application.references.list_references_pending_type_inference_conclusion.each do |reference|
|
|
9
|
+
Solve.call(application:, reference:)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/holistic.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "logger"
|
|
6
|
+
|
|
7
|
+
require "zeitwerk"
|
|
8
|
+
require "syntax_tree"
|
|
9
|
+
|
|
10
|
+
require "active_support/concern"
|
|
11
|
+
require "active_support/core_ext/module/concerning"
|
|
12
|
+
require "active_support/notifications"
|
|
13
|
+
require "active_support/inflector"
|
|
14
|
+
|
|
15
|
+
require_relative "../config/logging"
|
|
16
|
+
|
|
17
|
+
loader = ::Zeitwerk::Loader.for_gem
|
|
18
|
+
loader.setup
|
|
19
|
+
|
|
20
|
+
module Holistic
|
|
21
|
+
extend self
|
|
22
|
+
|
|
23
|
+
@logger = ::Logger.new(ENV["HOLISTIC_LOG_OUTPUT"])
|
|
24
|
+
attr_reader :logger
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
loader.eager_load
|