ruby-lsp 0.8.0 → 0.8.1
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 +6 -11
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +91 -0
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +122 -0
- data/lib/ruby_indexer/lib/ruby_indexer/visitor.rb +121 -0
- data/lib/ruby_indexer/ruby_indexer.rb +19 -0
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +204 -0
- data/lib/ruby_indexer/test/configuration_test.rb +35 -0
- data/lib/ruby_indexer/test/constant_test.rb +108 -0
- data/lib/ruby_indexer/test/index_test.rb +94 -0
- data/lib/ruby_indexer/test/test_case.rb +42 -0
- data/lib/ruby_lsp/executor.rb +110 -6
- data/lib/ruby_lsp/internal.rb +2 -0
- data/lib/ruby_lsp/requests/definition.rb +51 -4
- data/lib/ruby_lsp/requests/on_type_formatting.rb +4 -6
- data/lib/ruby_lsp/requests/support/dependency_detector.rb +5 -0
- data/lib/ruby_lsp/setup_bundler.rb +9 -5
- data/lib/ruby_lsp/store.rb +15 -9
- metadata +26 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b43407d4d6309c7243a6c69aca8166f6ab3548bd47e939c016efe26c1d8a4a76
|
4
|
+
data.tar.gz: cc5f638aab3322054cc2a09b60fe22a6e5ee8196d88492b4139907c2495c7d5d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dda98d73d8ba39cdfaffa3362177ef96887eccff10332012d43f79bdc78643d3bb79a9e43e05a5c21e4550cf9b86d8d00b5742fb51b124d2ed8c8112bc6f1d1a
|
7
|
+
data.tar.gz: 25a169a3eb717f5f11b5b911c91e11e578d33734f6f616c0c92fd6ebcc89d69ddda6abb13410ef231acc398ba0472e17c778d245b89f1285d64317df98c2299c
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.8.
|
1
|
+
0.8.1
|
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
@@ -9,7 +9,7 @@ module URI
|
|
9
9
|
sig { params(path: String, scheme: String).returns(URI::Generic) }
|
10
10
|
def from_path(path:, 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)
|
@@ -26,20 +26,15 @@ module URI
|
|
26
26
|
parsed_path = path
|
27
27
|
return unless parsed_path
|
28
28
|
|
29
|
+
unescaped_path = CGI.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,91 @@
|
|
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
|
+
}.freeze,
|
15
|
+
T::Hash[String, T::Class[Object]],
|
16
|
+
)
|
17
|
+
|
18
|
+
sig { void }
|
19
|
+
def initialize
|
20
|
+
development_only_dependencies = Bundler.definition.dependencies.filter_map do |dependency|
|
21
|
+
dependency.name if dependency.groups == [:development]
|
22
|
+
end
|
23
|
+
|
24
|
+
@excluded_gems = T.let(development_only_dependencies, T::Array[String])
|
25
|
+
@included_gems = T.let([], T::Array[String])
|
26
|
+
@excluded_patterns = T.let(["*_test.rb"], T::Array[String])
|
27
|
+
@included_patterns = T.let(["#{Dir.pwd}/**/*.rb"], T::Array[String])
|
28
|
+
end
|
29
|
+
|
30
|
+
sig { void }
|
31
|
+
def load_config
|
32
|
+
return unless File.exist?(".index.yml")
|
33
|
+
|
34
|
+
config = YAML.parse_file(".index.yml")
|
35
|
+
return unless config
|
36
|
+
|
37
|
+
config_hash = config.to_ruby
|
38
|
+
validate_config!(config_hash)
|
39
|
+
apply_config(config_hash)
|
40
|
+
rescue Psych::SyntaxError => e
|
41
|
+
raise e, "Syntax error while loading .index.yml configuration: #{e.message}"
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { returns(T::Array[String]) }
|
45
|
+
def files_to_index
|
46
|
+
files_to_index = $LOAD_PATH.flat_map { |p| Dir.glob("#{p}/**/*.rb", base: p) }
|
47
|
+
|
48
|
+
@included_patterns.each do |pattern|
|
49
|
+
files_to_index.concat(Dir.glob(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB))
|
50
|
+
end
|
51
|
+
|
52
|
+
excluded_gem_paths = (@excluded_gems - @included_gems).filter_map do |gem_name|
|
53
|
+
Gem::Specification.find_by_name(gem_name).full_gem_path
|
54
|
+
rescue Gem::MissingSpecError
|
55
|
+
warn("Gem #{gem_name} is excluded in .index.yml, but that gem was not found in the bundle")
|
56
|
+
end
|
57
|
+
|
58
|
+
files_to_index.reject! do |path|
|
59
|
+
@excluded_patterns.any? { |pattern| File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB) } ||
|
60
|
+
excluded_gem_paths.any? { |gem_path| File.fnmatch?("#{gem_path}/**/*.rb", path) }
|
61
|
+
end
|
62
|
+
files_to_index.uniq!
|
63
|
+
files_to_index
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
sig { params(config: T::Hash[String, T.untyped]).void }
|
69
|
+
def validate_config!(config)
|
70
|
+
errors = config.filter_map do |key, value|
|
71
|
+
type = CONFIGURATION_SCHEMA[key]
|
72
|
+
|
73
|
+
if type.nil?
|
74
|
+
"Unknown configuration option: #{key}"
|
75
|
+
elsif !value.is_a?(type)
|
76
|
+
"Expected #{key} to be a #{type}, but got #{value.class}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
raise ArgumentError, errors.join("\n") if errors.any?
|
81
|
+
end
|
82
|
+
|
83
|
+
sig { params(config: T::Hash[String, T.untyped]).void }
|
84
|
+
def apply_config(config)
|
85
|
+
@excluded_gems.concat(config["excluded_gems"]) if config["excluded_gems"]
|
86
|
+
@included_gems.concat(config["included_gems"]) if config["included_gems"]
|
87
|
+
@excluded_patterns.concat(config["excluded_patterns"]) if config["excluded_patterns"]
|
88
|
+
@included_patterns.concat(config["included_patterns"]) if config["included_patterns"]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyIndexer
|
5
|
+
class Index
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { void }
|
9
|
+
def initialize
|
10
|
+
# Holds all entries in the index using the following format:
|
11
|
+
# {
|
12
|
+
# "Foo" => [#<Entry::Class>, #<Entry::Class>],
|
13
|
+
# "Foo::Bar" => [#<Entry::Class>],
|
14
|
+
# }
|
15
|
+
@entries = T.let({}, T::Hash[String, T::Array[Entry]])
|
16
|
+
|
17
|
+
# Holds references to where entries where discovered so that we can easily delete them
|
18
|
+
# {
|
19
|
+
# "/my/project/foo.rb" => [#<Entry::Class>, #<Entry::Class>],
|
20
|
+
# "/my/project/bar.rb" => [#<Entry::Class>],
|
21
|
+
# }
|
22
|
+
@files_to_entries = T.let({}, T::Hash[String, T::Array[Entry]])
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { params(path: String).void }
|
26
|
+
def delete(path)
|
27
|
+
# For each constant discovered in `path`, delete the associated entry from the index. If there are no entries
|
28
|
+
# left, delete the constant from the index.
|
29
|
+
@files_to_entries[path]&.each do |entry|
|
30
|
+
entries = @entries[entry.name]
|
31
|
+
next unless entries
|
32
|
+
|
33
|
+
# Delete the specific entry from the list for this name
|
34
|
+
entries.delete(entry)
|
35
|
+
# If all entries were deleted, then remove the name from the hash
|
36
|
+
@entries.delete(entry.name) if entries.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
@files_to_entries.delete(path)
|
40
|
+
end
|
41
|
+
|
42
|
+
sig { params(entry: Entry).void }
|
43
|
+
def <<(entry)
|
44
|
+
(@entries[entry.name] ||= []) << entry
|
45
|
+
(@files_to_entries[entry.file_path] ||= []) << entry
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { params(fully_qualified_name: String).returns(T.nilable(T::Array[Entry])) }
|
49
|
+
def [](fully_qualified_name)
|
50
|
+
@entries[fully_qualified_name.delete_prefix("::")]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Try to find the entry based on the nesting from the most specific to the least specific. For example, if we have
|
54
|
+
# the nesting as ["Foo", "Bar"] and the name as "Baz", we will try to find it in this order:
|
55
|
+
# 1. Foo::Bar::Baz
|
56
|
+
# 2. Foo::Baz
|
57
|
+
# 3. Baz
|
58
|
+
sig { params(name: String, nesting: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
|
59
|
+
def resolve(name, nesting)
|
60
|
+
(nesting.length + 1).downto(0).each do |i|
|
61
|
+
prefix = T.must(nesting[0...i]).join("::")
|
62
|
+
full_name = prefix.empty? ? name : "#{prefix}::#{name}"
|
63
|
+
entries = @entries[full_name]
|
64
|
+
return entries if entries
|
65
|
+
end
|
66
|
+
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
sig { params(paths: T::Array[String]).void }
|
71
|
+
def index_all(paths: RubyIndexer.configuration.files_to_index)
|
72
|
+
paths.each { |path| index_single(path) }
|
73
|
+
end
|
74
|
+
|
75
|
+
sig { params(path: String, source: T.nilable(String)).void }
|
76
|
+
def index_single(path, source = nil)
|
77
|
+
content = source || File.read(path)
|
78
|
+
visitor = IndexVisitor.new(self, YARP.parse(content), path)
|
79
|
+
visitor.run
|
80
|
+
end
|
81
|
+
|
82
|
+
class Entry
|
83
|
+
extend T::Sig
|
84
|
+
|
85
|
+
sig { returns(String) }
|
86
|
+
attr_reader :name
|
87
|
+
|
88
|
+
sig { returns(String) }
|
89
|
+
attr_reader :file_path
|
90
|
+
|
91
|
+
sig { returns(YARP::Location) }
|
92
|
+
attr_reader :location
|
93
|
+
|
94
|
+
sig { returns(T::Array[String]) }
|
95
|
+
attr_reader :comments
|
96
|
+
|
97
|
+
sig { params(name: String, file_path: String, location: YARP::Location, comments: T::Array[String]).void }
|
98
|
+
def initialize(name, file_path, location, comments)
|
99
|
+
@name = name
|
100
|
+
@file_path = file_path
|
101
|
+
@location = location
|
102
|
+
@comments = comments
|
103
|
+
end
|
104
|
+
|
105
|
+
class Namespace < Entry
|
106
|
+
sig { returns(String) }
|
107
|
+
def short_name
|
108
|
+
T.must(@name.split("::").last)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class Module < Namespace
|
113
|
+
end
|
114
|
+
|
115
|
+
class Class < Namespace
|
116
|
+
end
|
117
|
+
|
118
|
+
class Constant < Entry
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,121 @@
|
|
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::ConstantOperatorOrWriteNode
|
39
|
+
add_constant(node)
|
40
|
+
when YARP::ConstantPathWriteNode, YARP::ConstantPathOperatorOrWriteNode
|
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::ConstantOperatorOrWriteNode),
|
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::ConstantPathOperatorOrWriteNode),
|
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
|
+
fully_qualified_name = name.start_with?("::") ? name.delete_prefix("::") : fully_qualify_name(name)
|
74
|
+
|
75
|
+
comments = collect_comments(node)
|
76
|
+
@index << Index::Entry::Constant.new(fully_qualified_name, @file_path, node.location, comments)
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { params(node: T.any(YARP::ClassNode, YARP::ModuleNode), klass: T.class_of(Index::Entry)).void }
|
80
|
+
def add_index_entry(node, klass)
|
81
|
+
name = node.constant_path.location.slice
|
82
|
+
|
83
|
+
unless /^[A-Z:]/.match?(name)
|
84
|
+
return visit_child_nodes(node)
|
85
|
+
end
|
86
|
+
|
87
|
+
fully_qualified_name = name.start_with?("::") ? name : fully_qualify_name(name)
|
88
|
+
name.delete_prefix!("::")
|
89
|
+
|
90
|
+
comments = collect_comments(node)
|
91
|
+
@index << klass.new(fully_qualified_name, @file_path, node.location, comments)
|
92
|
+
@stack << name
|
93
|
+
visit_child_nodes(node)
|
94
|
+
@stack.pop
|
95
|
+
end
|
96
|
+
|
97
|
+
sig { params(node: YARP::Node).returns(T::Array[String]) }
|
98
|
+
def collect_comments(node)
|
99
|
+
comments = []
|
100
|
+
|
101
|
+
start_line = node.location.start_line - 1
|
102
|
+
start_line -= 1 unless @comments_by_line.key?(start_line)
|
103
|
+
|
104
|
+
start_line.downto(1) do |line|
|
105
|
+
comment = @comments_by_line[line]
|
106
|
+
break unless comment
|
107
|
+
|
108
|
+
comments.unshift(comment.location.slice)
|
109
|
+
end
|
110
|
+
|
111
|
+
comments
|
112
|
+
end
|
113
|
+
|
114
|
+
sig { params(name: String).returns(String) }
|
115
|
+
def fully_qualify_name(name)
|
116
|
+
return name if @stack.empty?
|
117
|
+
|
118
|
+
"#{@stack.join("::")}::#{name}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "yaml"
|
5
|
+
|
6
|
+
require "ruby_indexer/lib/ruby_indexer/visitor"
|
7
|
+
require "ruby_indexer/lib/ruby_indexer/index"
|
8
|
+
require "ruby_indexer/lib/ruby_indexer/configuration"
|
9
|
+
|
10
|
+
module RubyIndexer
|
11
|
+
class << self
|
12
|
+
extend T::Sig
|
13
|
+
|
14
|
+
sig { returns(Configuration) }
|
15
|
+
def configuration
|
16
|
+
@configuration ||= T.let(Configuration.new, T.nilable(Configuration))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "test_case"
|
5
|
+
|
6
|
+
module RubyIndexer
|
7
|
+
class ClassesAndModulesTest < TestCase
|
8
|
+
def test_empty_statements_class
|
9
|
+
index(<<~RUBY)
|
10
|
+
class Foo
|
11
|
+
end
|
12
|
+
RUBY
|
13
|
+
|
14
|
+
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-2")
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_class_with_statements
|
18
|
+
index(<<~RUBY)
|
19
|
+
class Foo
|
20
|
+
def something; end
|
21
|
+
end
|
22
|
+
RUBY
|
23
|
+
|
24
|
+
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:2-2")
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_colon_colon_class
|
28
|
+
index(<<~RUBY)
|
29
|
+
class ::Foo
|
30
|
+
end
|
31
|
+
RUBY
|
32
|
+
|
33
|
+
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-2")
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_colon_colon_class_inside_class
|
37
|
+
index(<<~RUBY)
|
38
|
+
class Bar
|
39
|
+
class ::Foo
|
40
|
+
end
|
41
|
+
end
|
42
|
+
RUBY
|
43
|
+
|
44
|
+
assert_entry("Bar", Index::Entry::Class, "/fake/path/foo.rb:0-0:3-2")
|
45
|
+
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:1-2:2-4")
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_namespaced_class
|
49
|
+
index(<<~RUBY)
|
50
|
+
class Foo::Bar
|
51
|
+
end
|
52
|
+
RUBY
|
53
|
+
|
54
|
+
assert_entry("Foo::Bar", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-2")
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_dynamically_namespaced_class
|
58
|
+
index(<<~RUBY)
|
59
|
+
class self::Bar
|
60
|
+
end
|
61
|
+
RUBY
|
62
|
+
|
63
|
+
refute_entry("self::Bar")
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_empty_statements_module
|
67
|
+
index(<<~RUBY)
|
68
|
+
module Foo
|
69
|
+
end
|
70
|
+
RUBY
|
71
|
+
|
72
|
+
assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-2")
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_module_with_statements
|
76
|
+
index(<<~RUBY)
|
77
|
+
module Foo
|
78
|
+
def something; end
|
79
|
+
end
|
80
|
+
RUBY
|
81
|
+
|
82
|
+
assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:2-2")
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_colon_colon_module
|
86
|
+
index(<<~RUBY)
|
87
|
+
module ::Foo
|
88
|
+
end
|
89
|
+
RUBY
|
90
|
+
|
91
|
+
assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-2")
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_namespaced_module
|
95
|
+
index(<<~RUBY)
|
96
|
+
module Foo::Bar
|
97
|
+
end
|
98
|
+
RUBY
|
99
|
+
|
100
|
+
assert_entry("Foo::Bar", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-2")
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_dynamically_namespaced_module
|
104
|
+
index(<<~RUBY)
|
105
|
+
module self::Bar
|
106
|
+
end
|
107
|
+
RUBY
|
108
|
+
|
109
|
+
refute_entry("self::Bar")
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_nested_modules_and_classes
|
113
|
+
index(<<~RUBY)
|
114
|
+
module Foo
|
115
|
+
class Bar
|
116
|
+
end
|
117
|
+
|
118
|
+
module Baz
|
119
|
+
class Qux
|
120
|
+
class Something
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
RUBY
|
126
|
+
|
127
|
+
assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:10-2")
|
128
|
+
assert_entry("Foo::Bar", Index::Entry::Class, "/fake/path/foo.rb:1-2:2-4")
|
129
|
+
assert_entry("Foo::Baz", Index::Entry::Module, "/fake/path/foo.rb:4-2:9-4")
|
130
|
+
assert_entry("Foo::Baz::Qux", Index::Entry::Class, "/fake/path/foo.rb:5-4:8-6")
|
131
|
+
assert_entry("Foo::Baz::Qux::Something", Index::Entry::Class, "/fake/path/foo.rb:6-6:7-8")
|
132
|
+
end
|
133
|
+
|
134
|
+
def test_deleting_from_index_based_on_file_path
|
135
|
+
index(<<~RUBY)
|
136
|
+
class Foo
|
137
|
+
end
|
138
|
+
RUBY
|
139
|
+
|
140
|
+
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-2")
|
141
|
+
|
142
|
+
@index.delete("/fake/path/foo.rb")
|
143
|
+
refute_entry("Foo")
|
144
|
+
assert_empty(@index.instance_variable_get(:@files_to_entries))
|
145
|
+
end
|
146
|
+
|
147
|
+
def test_comments_can_be_attached_to_a_class
|
148
|
+
index(<<~RUBY)
|
149
|
+
# This is method comment
|
150
|
+
def foo; end
|
151
|
+
# This is a Foo comment
|
152
|
+
# This is another Foo comment
|
153
|
+
class Foo
|
154
|
+
# This should not be attached
|
155
|
+
end
|
156
|
+
|
157
|
+
# Ignore me
|
158
|
+
|
159
|
+
# This Bar comment has 1 line padding
|
160
|
+
|
161
|
+
class Bar; end
|
162
|
+
RUBY
|
163
|
+
|
164
|
+
foo_entry = @index["Foo"].first
|
165
|
+
assert_equal("# This is a Foo comment\n# This is another Foo comment\n", foo_entry.comments.join)
|
166
|
+
|
167
|
+
bar_entry = @index["Bar"].first
|
168
|
+
assert_equal("# This Bar comment has 1 line padding\n", bar_entry.comments.join)
|
169
|
+
end
|
170
|
+
|
171
|
+
def test_comments_can_be_attached_to_a_namespaced_class
|
172
|
+
index(<<~RUBY)
|
173
|
+
# This is a Foo comment
|
174
|
+
# This is another Foo comment
|
175
|
+
class Foo
|
176
|
+
# This is a Bar comment
|
177
|
+
class Bar; end
|
178
|
+
end
|
179
|
+
RUBY
|
180
|
+
|
181
|
+
foo_entry = @index["Foo"].first
|
182
|
+
assert_equal("# This is a Foo comment\n# This is another Foo comment\n", foo_entry.comments.join)
|
183
|
+
|
184
|
+
bar_entry = @index["Foo::Bar"].first
|
185
|
+
assert_equal("# This is a Bar comment\n", bar_entry.comments.join)
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_comments_can_be_attached_to_a_reopened_class
|
189
|
+
index(<<~RUBY)
|
190
|
+
# This is a Foo comment
|
191
|
+
class Foo; end
|
192
|
+
|
193
|
+
# This is another Foo comment
|
194
|
+
class Foo; end
|
195
|
+
RUBY
|
196
|
+
|
197
|
+
first_foo_entry = @index["Foo"][0]
|
198
|
+
assert_equal("# This is a Foo comment\n", first_foo_entry.comments.join)
|
199
|
+
|
200
|
+
second_foo_entry = @index["Foo"][1]
|
201
|
+
assert_equal("# This is another Foo comment\n", second_foo_entry.comments.join)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|