ruby-lsp 0.8.0 → 0.9.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 +4 -4
- data/VERSION +1 -1
- data/exe/ruby-lsp +41 -33
- data/lib/core_ext/uri.rb +9 -14
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +166 -0
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +147 -0
- data/lib/ruby_indexer/lib/ruby_indexer/visitor.rb +123 -0
- data/lib/ruby_indexer/ruby_indexer.rb +20 -0
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +220 -0
- data/lib/ruby_indexer/test/configuration_test.rb +114 -0
- data/lib/ruby_indexer/test/constant_test.rb +108 -0
- data/lib/ruby_indexer/test/index_test.rb +129 -0
- data/lib/ruby_indexer/test/test_case.rb +42 -0
- data/lib/ruby_lsp/executor.rb +144 -10
- data/lib/ruby_lsp/internal.rb +2 -0
- data/lib/ruby_lsp/requests/definition.rb +60 -5
- data/lib/ruby_lsp/requests/hover.rb +53 -30
- data/lib/ruby_lsp/requests/on_type_formatting.rb +4 -6
- data/lib/ruby_lsp/requests/support/dependency_detector.rb +7 -0
- data/lib/ruby_lsp/requests/workspace_symbol.rb +86 -0
- data/lib/ruby_lsp/requests.rb +1 -0
- data/lib/ruby_lsp/setup_bundler.rb +24 -8
- data/lib/ruby_lsp/store.rb +15 -9
- metadata +33 -4
- data/lib/ruby_lsp/requests/support/rails_document_client.rb +0 -122
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3dd7fa2d26430cf1974e1d94bbfa860166dd964ffe9268d9b307b323baa86954
|
4
|
+
data.tar.gz: 6183306fd3d34396de880184fe9a09cdafccd6cb071b7927e458f658d9397221
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed5e0524b0001497cd857dad621a6b072434375ef2be8583e12ddfcd2b7d104b279ab7ea026db09e563ddd908d7d575da682bf6a30244cd6aca68da6b4ee2ad6
|
7
|
+
data.tar.gz: 8de1bdb11dfb12c34129e3fddeae89c6b3700a74cb6483424523df9584a28a8b52499fade45d7c7c047ee517ed53a5a4f54b9fcc04a386a17b092999a264563a
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.9.0
|
data/exe/ruby-lsp
CHANGED
@@ -1,6 +1,46 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "optparse"
|
5
|
+
|
6
|
+
options = {}
|
7
|
+
parser = OptionParser.new do |opts|
|
8
|
+
opts.banner = "Usage: ruby-lsp [options]"
|
9
|
+
|
10
|
+
opts.on("--version", "Print ruby-lsp version") do
|
11
|
+
require "ruby-lsp"
|
12
|
+
puts RubyLsp::VERSION
|
13
|
+
exit(0)
|
14
|
+
end
|
15
|
+
|
16
|
+
opts.on("--debug", "Launch the Ruby LSP with a debugger attached") do
|
17
|
+
options[:debug] = true
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on(
|
21
|
+
"--branch [BRANCH]",
|
22
|
+
"Launch the Ruby LSP using the specified branch rather than the release version",
|
23
|
+
) do |branch|
|
24
|
+
options[:branch] = branch
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("-h", "--help", "Print this help") do
|
28
|
+
puts opts.help
|
29
|
+
puts
|
30
|
+
puts "See https://shopify.github.io/ruby-lsp/ for more information"
|
31
|
+
exit(0)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
begin
|
36
|
+
parser.parse
|
37
|
+
rescue OptionParser::InvalidOption => e
|
38
|
+
warn(e)
|
39
|
+
warn("")
|
40
|
+
warn(parser.help)
|
41
|
+
exit(1)
|
42
|
+
end
|
43
|
+
|
4
44
|
# When we're running without bundler, then we need to make sure the custom bundle is fully configured and re-execute
|
5
45
|
# using `BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle exec ruby-lsp` so that we have access to the gems that are a part of
|
6
46
|
# the application's bundle
|
@@ -8,7 +48,7 @@ if ENV["BUNDLE_GEMFILE"].nil?
|
|
8
48
|
require_relative "../lib/ruby_lsp/setup_bundler"
|
9
49
|
|
10
50
|
begin
|
11
|
-
bundle_gemfile, bundle_path = RubyLsp::SetupBundler.new(Dir.pwd).setup!
|
51
|
+
bundle_gemfile, bundle_path = RubyLsp::SetupBundler.new(Dir.pwd, branch: options[:branch]).setup!
|
12
52
|
rescue RubyLsp::SetupBundler::BundleNotLocked
|
13
53
|
warn("Project contains a Gemfile, but no Gemfile.lock. Run `bundle install` to lock gems and restart the server")
|
14
54
|
exit(78)
|
@@ -38,38 +78,6 @@ end
|
|
38
78
|
|
39
79
|
require_relative "../lib/ruby_lsp/internal"
|
40
80
|
|
41
|
-
require "optparse"
|
42
|
-
|
43
|
-
options = {}
|
44
|
-
parser = OptionParser.new do |opts|
|
45
|
-
opts.banner = "Usage: ruby-lsp [options]"
|
46
|
-
|
47
|
-
opts.on("--version", "Print ruby-lsp version") do
|
48
|
-
puts RubyLsp::VERSION
|
49
|
-
exit(0)
|
50
|
-
end
|
51
|
-
|
52
|
-
opts.on("--debug", "Launch the Ruby LSP with a debugger attached") do
|
53
|
-
options[:debug] = true
|
54
|
-
end
|
55
|
-
|
56
|
-
opts.on("-h", "--help", "Print this help") do
|
57
|
-
puts opts.help
|
58
|
-
puts
|
59
|
-
puts "See https://shopify.github.io/ruby-lsp/ for more information"
|
60
|
-
exit(0)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
begin
|
65
|
-
parser.parse!
|
66
|
-
rescue OptionParser::InvalidOption => e
|
67
|
-
warn(e)
|
68
|
-
warn("")
|
69
|
-
warn(parser.help)
|
70
|
-
exit(1)
|
71
|
-
end
|
72
|
-
|
73
81
|
if options[:debug]
|
74
82
|
if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
|
75
83
|
puts "Debugging is not supported on Windows"
|
data/lib/core_ext/uri.rb
CHANGED
@@ -6,16 +6,16 @@ module URI
|
|
6
6
|
class << self
|
7
7
|
extend T::Sig
|
8
8
|
|
9
|
-
sig { params(path: String, scheme: String).returns(URI::Generic) }
|
10
|
-
def from_path(path:, scheme: "file")
|
9
|
+
sig { params(path: String, fragment: T.nilable(String), scheme: String).returns(URI::Generic) }
|
10
|
+
def from_path(path:, fragment: nil, scheme: "file")
|
11
11
|
# On Windows, if the path begins with the disk name, we need to add a leading slash to make it a valid URI
|
12
|
-
escaped_path = if /^[A-Z]
|
12
|
+
escaped_path = if /^[A-Z]:/i.match?(path)
|
13
13
|
DEFAULT_PARSER.escape("/#{path}")
|
14
14
|
else
|
15
15
|
DEFAULT_PARSER.escape(path)
|
16
16
|
end
|
17
17
|
|
18
|
-
build(scheme: scheme, path: escaped_path)
|
18
|
+
build(scheme: scheme, path: escaped_path, fragment: fragment)
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
@@ -26,20 +26,15 @@ module URI
|
|
26
26
|
parsed_path = path
|
27
27
|
return unless parsed_path
|
28
28
|
|
29
|
+
unescaped_path = DEFAULT_PARSER.unescape(parsed_path)
|
30
|
+
|
29
31
|
# On Windows, when we're getting the file system path back from the URI, we need to remove the leading forward
|
30
32
|
# slash
|
31
|
-
|
32
|
-
|
33
|
+
if %r{^/[A-Z]:}i.match?(unescaped_path)
|
34
|
+
unescaped_path.delete_prefix("/")
|
33
35
|
else
|
34
|
-
|
36
|
+
unescaped_path
|
35
37
|
end
|
36
|
-
|
37
|
-
CGI.unescape(actual_path)
|
38
|
-
end
|
39
|
-
|
40
|
-
sig { returns(String) }
|
41
|
-
def storage_key
|
42
|
-
T.must(to_standardized_path || opaque)
|
43
38
|
end
|
44
39
|
end
|
45
40
|
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyIndexer
|
5
|
+
class Configuration
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
CONFIGURATION_SCHEMA = T.let(
|
9
|
+
{
|
10
|
+
"excluded_gems" => Array,
|
11
|
+
"included_gems" => Array,
|
12
|
+
"excluded_patterns" => Array,
|
13
|
+
"included_patterns" => Array,
|
14
|
+
"excluded_magic_comments" => Array,
|
15
|
+
}.freeze,
|
16
|
+
T::Hash[String, T.untyped],
|
17
|
+
)
|
18
|
+
|
19
|
+
sig { void }
|
20
|
+
def initialize
|
21
|
+
excluded_gem_names = Bundler.definition.dependencies.filter_map do |dependency|
|
22
|
+
dependency.name if dependency.groups == [:development]
|
23
|
+
end
|
24
|
+
|
25
|
+
@excluded_gems = T.let(excluded_gem_names, T::Array[String])
|
26
|
+
@included_gems = T.let([], T::Array[String])
|
27
|
+
@excluded_patterns = T.let(["**/*_test.rb"], T::Array[String])
|
28
|
+
path = Bundler.settings["path"]
|
29
|
+
@excluded_patterns << "#{File.expand_path(path, Dir.pwd)}/**/*.rb" if path
|
30
|
+
|
31
|
+
@included_patterns = T.let(["#{Dir.pwd}/**/*.rb"], T::Array[String])
|
32
|
+
@excluded_magic_comments = T.let(
|
33
|
+
[
|
34
|
+
"frozen_string_literal:",
|
35
|
+
"typed:",
|
36
|
+
"compiled:",
|
37
|
+
"encoding:",
|
38
|
+
"shareable_constant_value:",
|
39
|
+
"warn_indent:",
|
40
|
+
"rubocop:",
|
41
|
+
"nodoc:",
|
42
|
+
"doc:",
|
43
|
+
"coding:",
|
44
|
+
"warn_past_scope:",
|
45
|
+
],
|
46
|
+
T::Array[String],
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { void }
|
51
|
+
def load_config
|
52
|
+
return unless File.exist?(".index.yml")
|
53
|
+
|
54
|
+
config = YAML.parse_file(".index.yml")
|
55
|
+
return unless config
|
56
|
+
|
57
|
+
config_hash = config.to_ruby
|
58
|
+
validate_config!(config_hash)
|
59
|
+
apply_config(config_hash)
|
60
|
+
rescue Psych::SyntaxError => e
|
61
|
+
raise e, "Syntax error while loading .index.yml configuration: #{e.message}"
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { returns(T::Array[String]) }
|
65
|
+
def files_to_index
|
66
|
+
excluded_gems = @excluded_gems - @included_gems
|
67
|
+
locked_gems = Bundler.locked_gems&.specs
|
68
|
+
|
69
|
+
# NOTE: indexing the patterns (both included and excluded) needs to happen before indexing gems, otherwise we risk
|
70
|
+
# having duplicates if BUNDLE_PATH is set to a folder inside the project structure
|
71
|
+
|
72
|
+
# Add user specified patterns
|
73
|
+
files_to_index = @included_patterns.flat_map do |pattern|
|
74
|
+
Dir.glob(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Remove user specified patterns
|
78
|
+
files_to_index.reject! do |path|
|
79
|
+
@excluded_patterns.any? do |pattern|
|
80
|
+
File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Add default gems to the list of files to be indexed
|
85
|
+
Dir.glob("#{RbConfig::CONFIG["rubylibdir"]}/*").each do |default_path|
|
86
|
+
# The default_path might be a Ruby file or a folder with the gem's name. For example:
|
87
|
+
# bundler/
|
88
|
+
# bundler.rb
|
89
|
+
# psych/
|
90
|
+
# psych.rb
|
91
|
+
pathname = Pathname.new(default_path)
|
92
|
+
short_name = pathname.basename.to_s.delete_suffix(".rb")
|
93
|
+
|
94
|
+
# If the gem name is excluded, then we skip it
|
95
|
+
next if excluded_gems.include?(short_name)
|
96
|
+
|
97
|
+
# If the default gem is also a part of the bundle, we skip indexing the default one and index only the one in
|
98
|
+
# the bundle, which won't be in `default_path`, but will be in `Bundler.bundle_path` instead
|
99
|
+
next if locked_gems&.any? do |locked_spec|
|
100
|
+
locked_spec.name == short_name &&
|
101
|
+
!Gem::Specification.find_by_name(short_name).full_gem_path.start_with?(RbConfig::CONFIG["rubylibprefix"])
|
102
|
+
end
|
103
|
+
|
104
|
+
if pathname.directory?
|
105
|
+
# If the default_path is a directory, we index all the Ruby files in it
|
106
|
+
files_to_index.concat(Dir.glob("#{default_path}/**/*.rb", File::FNM_PATHNAME | File::FNM_EXTGLOB))
|
107
|
+
else
|
108
|
+
# If the default_path is a Ruby file, we index it
|
109
|
+
files_to_index << default_path
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Add the locked gems to the list of files to be indexed
|
114
|
+
locked_gems&.each do |lazy_spec|
|
115
|
+
next if excluded_gems.include?(lazy_spec.name)
|
116
|
+
|
117
|
+
spec = Gem::Specification.find_by_name(lazy_spec.name)
|
118
|
+
|
119
|
+
# When working on a gem, it will be included in the locked_gems list. Since these are the project's own files,
|
120
|
+
# we have already included and handled exclude patterns for it and should not re-include or it'll lead to
|
121
|
+
# duplicates or accidentally ignoring exclude patterns
|
122
|
+
next if spec.full_gem_path == Dir.pwd
|
123
|
+
|
124
|
+
files_to_index.concat(Dir.glob("#{spec.full_gem_path}/{#{spec.require_paths.join(",")}}/**/*.rb"))
|
125
|
+
rescue Gem::MissingSpecError
|
126
|
+
# If a gem is scoped only to some specific platform, then its dependencies may not be installed either, but they
|
127
|
+
# are still listed in locked_gems. We can't index them because they are not installed for the platform, so we
|
128
|
+
# just ignore if they're missing
|
129
|
+
end
|
130
|
+
|
131
|
+
files_to_index.uniq!
|
132
|
+
files_to_index
|
133
|
+
end
|
134
|
+
|
135
|
+
sig { returns(Regexp) }
|
136
|
+
def magic_comment_regex
|
137
|
+
/^\s*#\s*#{@excluded_magic_comments.join("|")}/
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
sig { params(config: T::Hash[String, T.untyped]).void }
|
143
|
+
def validate_config!(config)
|
144
|
+
errors = config.filter_map do |key, value|
|
145
|
+
type = CONFIGURATION_SCHEMA[key]
|
146
|
+
|
147
|
+
if type.nil?
|
148
|
+
"Unknown configuration option: #{key}"
|
149
|
+
elsif !value.is_a?(type)
|
150
|
+
"Expected #{key} to be a #{type}, but got #{value.class}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
raise ArgumentError, errors.join("\n") if errors.any?
|
155
|
+
end
|
156
|
+
|
157
|
+
sig { params(config: T::Hash[String, T.untyped]).void }
|
158
|
+
def apply_config(config)
|
159
|
+
@excluded_gems.concat(config["excluded_gems"]) if config["excluded_gems"]
|
160
|
+
@included_gems.concat(config["included_gems"]) if config["included_gems"]
|
161
|
+
@excluded_patterns.concat(config["excluded_patterns"]) if config["excluded_patterns"]
|
162
|
+
@included_patterns.concat(config["included_patterns"]) if config["included_patterns"]
|
163
|
+
@excluded_magic_comments.concat(config["excluded_magic_comments"]) if config["excluded_magic_comments"]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyIndexer
|
5
|
+
class Index
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
# The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
|
9
|
+
ENTRY_SIMILARITY_THRESHOLD = 0.7
|
10
|
+
|
11
|
+
sig { void }
|
12
|
+
def initialize
|
13
|
+
# Holds all entries in the index using the following format:
|
14
|
+
# {
|
15
|
+
# "Foo" => [#<Entry::Class>, #<Entry::Class>],
|
16
|
+
# "Foo::Bar" => [#<Entry::Class>],
|
17
|
+
# }
|
18
|
+
@entries = T.let({}, T::Hash[String, T::Array[Entry]])
|
19
|
+
|
20
|
+
# Holds references to where entries where discovered so that we can easily delete them
|
21
|
+
# {
|
22
|
+
# "/my/project/foo.rb" => [#<Entry::Class>, #<Entry::Class>],
|
23
|
+
# "/my/project/bar.rb" => [#<Entry::Class>],
|
24
|
+
# }
|
25
|
+
@files_to_entries = T.let({}, T::Hash[String, T::Array[Entry]])
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { params(path: String).void }
|
29
|
+
def delete(path)
|
30
|
+
# For each constant discovered in `path`, delete the associated entry from the index. If there are no entries
|
31
|
+
# left, delete the constant from the index.
|
32
|
+
@files_to_entries[path]&.each do |entry|
|
33
|
+
entries = @entries[entry.name]
|
34
|
+
next unless entries
|
35
|
+
|
36
|
+
# Delete the specific entry from the list for this name
|
37
|
+
entries.delete(entry)
|
38
|
+
# If all entries were deleted, then remove the name from the hash
|
39
|
+
@entries.delete(entry.name) if entries.empty?
|
40
|
+
end
|
41
|
+
|
42
|
+
@files_to_entries.delete(path)
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { params(entry: Entry).void }
|
46
|
+
def <<(entry)
|
47
|
+
(@entries[entry.name] ||= []) << entry
|
48
|
+
(@files_to_entries[entry.file_path] ||= []) << entry
|
49
|
+
end
|
50
|
+
|
51
|
+
sig { params(fully_qualified_name: String).returns(T.nilable(T::Array[Entry])) }
|
52
|
+
def [](fully_qualified_name)
|
53
|
+
@entries[fully_qualified_name.delete_prefix("::")]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned
|
57
|
+
sig { params(query: T.nilable(String)).returns(T::Array[Entry]) }
|
58
|
+
def fuzzy_search(query)
|
59
|
+
return @entries.flat_map { |_name, entries| entries } unless query
|
60
|
+
|
61
|
+
normalized_query = query.gsub("::", "").downcase
|
62
|
+
|
63
|
+
results = @entries.filter_map do |name, entries|
|
64
|
+
similarity = DidYouMean::JaroWinkler.distance(name.gsub("::", "").downcase, normalized_query)
|
65
|
+
[entries, -similarity] if similarity > ENTRY_SIMILARITY_THRESHOLD
|
66
|
+
end
|
67
|
+
results.sort_by!(&:last)
|
68
|
+
results.flat_map(&:first)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Try to find the entry based on the nesting from the most specific to the least specific. For example, if we have
|
72
|
+
# the nesting as ["Foo", "Bar"] and the name as "Baz", we will try to find it in this order:
|
73
|
+
# 1. Foo::Bar::Baz
|
74
|
+
# 2. Foo::Baz
|
75
|
+
# 3. Baz
|
76
|
+
sig { params(name: String, nesting: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
|
77
|
+
def resolve(name, nesting)
|
78
|
+
(nesting.length + 1).downto(0).each do |i|
|
79
|
+
prefix = T.must(nesting[0...i]).join("::")
|
80
|
+
full_name = prefix.empty? ? name : "#{prefix}::#{name}"
|
81
|
+
entries = @entries[full_name]
|
82
|
+
return entries if entries
|
83
|
+
end
|
84
|
+
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
sig { params(paths: T::Array[String]).void }
|
89
|
+
def index_all(paths: RubyIndexer.configuration.files_to_index)
|
90
|
+
paths.each { |path| index_single(path) }
|
91
|
+
end
|
92
|
+
|
93
|
+
sig { params(path: String, source: T.nilable(String)).void }
|
94
|
+
def index_single(path, source = nil)
|
95
|
+
content = source || File.read(path)
|
96
|
+
visitor = IndexVisitor.new(self, YARP.parse(content), path)
|
97
|
+
visitor.run
|
98
|
+
rescue Errno::EISDIR
|
99
|
+
# If `path` is a directory, just ignore it and continue indexing
|
100
|
+
end
|
101
|
+
|
102
|
+
class Entry
|
103
|
+
extend T::Sig
|
104
|
+
|
105
|
+
sig { returns(String) }
|
106
|
+
attr_reader :name
|
107
|
+
|
108
|
+
sig { returns(String) }
|
109
|
+
attr_reader :file_path
|
110
|
+
|
111
|
+
sig { returns(YARP::Location) }
|
112
|
+
attr_reader :location
|
113
|
+
|
114
|
+
sig { returns(T::Array[String]) }
|
115
|
+
attr_reader :comments
|
116
|
+
|
117
|
+
sig { params(name: String, file_path: String, location: YARP::Location, comments: T::Array[String]).void }
|
118
|
+
def initialize(name, file_path, location, comments)
|
119
|
+
@name = name
|
120
|
+
@file_path = file_path
|
121
|
+
@location = location
|
122
|
+
@comments = comments
|
123
|
+
end
|
124
|
+
|
125
|
+
sig { returns(String) }
|
126
|
+
def file_name
|
127
|
+
File.basename(@file_path)
|
128
|
+
end
|
129
|
+
|
130
|
+
class Namespace < Entry
|
131
|
+
sig { returns(String) }
|
132
|
+
def short_name
|
133
|
+
T.must(@name.split("::").last)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class Module < Namespace
|
138
|
+
end
|
139
|
+
|
140
|
+
class Class < Namespace
|
141
|
+
end
|
142
|
+
|
143
|
+
class Constant < Entry
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyIndexer
|
5
|
+
class IndexVisitor < YARP::Visitor
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { params(index: Index, parse_result: YARP::ParseResult, file_path: String).void }
|
9
|
+
def initialize(index, parse_result, file_path)
|
10
|
+
@index = index
|
11
|
+
@parse_result = parse_result
|
12
|
+
@file_path = file_path
|
13
|
+
@stack = T.let([], T::Array[String])
|
14
|
+
@comments_by_line = T.let(
|
15
|
+
parse_result.comments.to_h do |c|
|
16
|
+
[c.location.start_line, c]
|
17
|
+
end,
|
18
|
+
T::Hash[Integer, YARP::Comment],
|
19
|
+
)
|
20
|
+
|
21
|
+
super()
|
22
|
+
end
|
23
|
+
|
24
|
+
sig { void }
|
25
|
+
def run
|
26
|
+
visit(@parse_result.value)
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { params(node: T.nilable(YARP::Node)).void }
|
30
|
+
def visit(node)
|
31
|
+
case node
|
32
|
+
when YARP::ProgramNode, YARP::StatementsNode
|
33
|
+
visit_child_nodes(node)
|
34
|
+
when YARP::ClassNode
|
35
|
+
add_index_entry(node, Index::Entry::Class)
|
36
|
+
when YARP::ModuleNode
|
37
|
+
add_index_entry(node, Index::Entry::Module)
|
38
|
+
when YARP::ConstantWriteNode, YARP::ConstantOrWriteNode
|
39
|
+
add_constant(node)
|
40
|
+
when YARP::ConstantPathWriteNode, YARP::ConstantPathOrWriteNode
|
41
|
+
add_constant_with_path(node)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Override to avoid using `map` instead of `each`
|
46
|
+
sig { params(nodes: T::Array[T.nilable(YARP::Node)]).void }
|
47
|
+
def visit_all(nodes)
|
48
|
+
nodes.each { |node| visit(node) }
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
sig do
|
54
|
+
params(
|
55
|
+
node: T.any(YARP::ConstantWriteNode, YARP::ConstantOrWriteNode),
|
56
|
+
).void
|
57
|
+
end
|
58
|
+
def add_constant(node)
|
59
|
+
comments = collect_comments(node)
|
60
|
+
@index << Index::Entry::Constant.new(fully_qualify_name(node.name), @file_path, node.location, comments)
|
61
|
+
end
|
62
|
+
|
63
|
+
sig do
|
64
|
+
params(
|
65
|
+
node: T.any(YARP::ConstantPathWriteNode, YARP::ConstantPathOrWriteNode),
|
66
|
+
).void
|
67
|
+
end
|
68
|
+
def add_constant_with_path(node)
|
69
|
+
# ignore variable constants like `var::FOO` or `self.class::FOO`
|
70
|
+
return unless node.target.parent.nil? || node.target.parent.is_a?(YARP::ConstantReadNode)
|
71
|
+
|
72
|
+
name = node.target.location.slice
|
73
|
+
comments = collect_comments(node)
|
74
|
+
@index << Index::Entry::Constant.new(fully_qualify_name(name), @file_path, node.location, comments)
|
75
|
+
end
|
76
|
+
|
77
|
+
sig { params(node: T.any(YARP::ClassNode, YARP::ModuleNode), klass: T.class_of(Index::Entry)).void }
|
78
|
+
def add_index_entry(node, klass)
|
79
|
+
name = node.constant_path.location.slice
|
80
|
+
|
81
|
+
unless /^[A-Z:]/.match?(name)
|
82
|
+
return visit_child_nodes(node)
|
83
|
+
end
|
84
|
+
|
85
|
+
comments = collect_comments(node)
|
86
|
+
@index << klass.new(fully_qualify_name(name), @file_path, node.location, comments)
|
87
|
+
@stack << name
|
88
|
+
visit_child_nodes(node)
|
89
|
+
@stack.pop
|
90
|
+
end
|
91
|
+
|
92
|
+
sig { params(node: YARP::Node).returns(T::Array[String]) }
|
93
|
+
def collect_comments(node)
|
94
|
+
comments = []
|
95
|
+
|
96
|
+
start_line = node.location.start_line - 1
|
97
|
+
start_line -= 1 unless @comments_by_line.key?(start_line)
|
98
|
+
|
99
|
+
start_line.downto(1) do |line|
|
100
|
+
comment = @comments_by_line[line]
|
101
|
+
break unless comment
|
102
|
+
|
103
|
+
comment_content = comment.location.slice.chomp
|
104
|
+
next if comment_content.match?(RubyIndexer.configuration.magic_comment_regex)
|
105
|
+
|
106
|
+
comment_content.delete_prefix!("#")
|
107
|
+
comment_content.delete_prefix!(" ")
|
108
|
+
comments.unshift(comment_content)
|
109
|
+
end
|
110
|
+
|
111
|
+
comments
|
112
|
+
end
|
113
|
+
|
114
|
+
sig { params(name: String).returns(String) }
|
115
|
+
def fully_qualify_name(name)
|
116
|
+
if @stack.empty? || name.start_with?("::")
|
117
|
+
name
|
118
|
+
else
|
119
|
+
"#{@stack.join("::")}::#{name}"
|
120
|
+
end.delete_prefix("::")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "yaml"
|
5
|
+
require "did_you_mean"
|
6
|
+
|
7
|
+
require "ruby_indexer/lib/ruby_indexer/visitor"
|
8
|
+
require "ruby_indexer/lib/ruby_indexer/index"
|
9
|
+
require "ruby_indexer/lib/ruby_indexer/configuration"
|
10
|
+
|
11
|
+
module RubyIndexer
|
12
|
+
class << self
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
sig { returns(Configuration) }
|
16
|
+
def configuration
|
17
|
+
@configuration ||= T.let(Configuration.new, T.nilable(Configuration))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|