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