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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed62114ae68bf84cf95f96a2517bc2e729ebaf572500b3871f253bc4b584d7ba
4
- data.tar.gz: d34dd2719e3b8088f59f4c38988b3fa7058c866a0d34b8b0b0ea391e18ec64d2
3
+ metadata.gz: 3dd7fa2d26430cf1974e1d94bbfa860166dd964ffe9268d9b307b323baa86954
4
+ data.tar.gz: 6183306fd3d34396de880184fe9a09cdafccd6cb071b7927e458f658d9397221
5
5
  SHA512:
6
- metadata.gz: d9d7bdf1a6ce4d3a7711a0032236d25d3b79682239f801354c15c01661a8e3b1110be90dd503e1d75dcd442520893d6cc852ef5a1eaeeb5a0834305a1c3350c7
7
- data.tar.gz: f55f82e138cb65fb7450afe86092d294bca06bcd6043439723567ed6538a1b373eed10ba5f3702e9d17f16fc4faa6b5447c048fb5683388e52bea1588d23dc63
6
+ metadata.gz: ed5e0524b0001497cd857dad621a6b072434375ef2be8583e12ddfcd2b7d104b279ab7ea026db09e563ddd908d7d575da682bf6a30244cd6aca68da6b4ee2ad6
7
+ data.tar.gz: 8de1bdb11dfb12c34129e3fddeae89c6b3700a74cb6483424523df9584a28a8b52499fade45d7c7c047ee517ed53a5a4f54b9fcc04a386a17b092999a264563a
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.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]:/.match?(path)
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
- actual_path = if %r{^/[A-Z]:}.match?(parsed_path)
32
- parsed_path.delete_prefix("/")
33
+ if %r{^/[A-Z]:}i.match?(unescaped_path)
34
+ unescaped_path.delete_prefix("/")
33
35
  else
34
- parsed_path
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