ruby-lsp 0.8.0 → 0.8.1
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 +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
|