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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed62114ae68bf84cf95f96a2517bc2e729ebaf572500b3871f253bc4b584d7ba
4
- data.tar.gz: d34dd2719e3b8088f59f4c38988b3fa7058c866a0d34b8b0b0ea391e18ec64d2
3
+ metadata.gz: b43407d4d6309c7243a6c69aca8166f6ab3548bd47e939c016efe26c1d8a4a76
4
+ data.tar.gz: cc5f638aab3322054cc2a09b60fe22a6e5ee8196d88492b4139907c2495c7d5d
5
5
  SHA512:
6
- metadata.gz: d9d7bdf1a6ce4d3a7711a0032236d25d3b79682239f801354c15c01661a8e3b1110be90dd503e1d75dcd442520893d6cc852ef5a1eaeeb5a0834305a1c3350c7
7
- data.tar.gz: f55f82e138cb65fb7450afe86092d294bca06bcd6043439723567ed6538a1b373eed10ba5f3702e9d17f16fc4faa6b5447c048fb5683388e52bea1588d23dc63
6
+ metadata.gz: dda98d73d8ba39cdfaffa3362177ef96887eccff10332012d43f79bdc78643d3bb79a9e43e05a5c21e4550cf9b86d8d00b5742fb51b124d2ed8c8112bc6f1d1a
7
+ data.tar.gz: 25a169a3eb717f5f11b5b911c91e11e578d33734f6f616c0c92fd6ebcc89d69ddda6abb13410ef231acc398ba0472e17c778d245b89f1285d64317df98c2299c
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.0
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]:/.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)
@@ -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
- 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,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