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 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