Repository: Shopify/ruby-lsp (Ruby)

Issue:
Ignore "frozen_string_literal" pragma comments when indexing code documentation
### I have checked that this feature is not already implemented

- [x] This feature does not exist

### Use case

I want to see documentation for a Ruby class, but currently the Ruby LSP sometimes aggregates pragma comments along with everything else.

### Description

I have a class like this in my app:

```rb
# frozen_string_literal: true
class Foo < Bar
  def whatever
    puts 'qux'
  end
end
```

If I hover over a reference to this class elsewhere in the codebase, it will show "frozen_string_literal: true" as the documentation in the hover.

<img width="328" height="187" alt="Image" src="https://github.com/user-attachments/assets/43c9a5ad-9846-4d61-a908-a31fef0753b5" />

In some cases, this can lead to it showing up repeatedly for a given class, like here for the pg_search gem:

<img width="989" height="336" alt="Image" src="https://github.com/user-attachments/assets/83053781-6702-4141-9200-59e40ba58986" />

### Implementation

If the only comment that is found for a class is `# frozen_string_literal: true`, the comment should be discarded.

There are cases where a comment may intentionally have this line/string in it, and so it should probably only discard the comment if that's the _entire_ comment. I guess you could also discard it if it's the first line in a comment, for example if my code was like this:

```rb
# frozen_string_literal: true
# This class is used for blah blah blah blah.
class Foo < Bar
  def whatever
    puts 'qux'
  end
end
```

I realize you generally should have an empty line after the pragma, before any code/real comments (and many codebases have rubocop enforce that anyway), but I can't control it for third party gems, and so I still think this is worth considering to fix.


Discussion:
Thanks for reporting. This was an oversight. We perform the exclusion of magic comments when eagerly indexing comments [here](https://github.com/Shopify/ruby-lsp/blob/9e7450dfe690a399aa25dff4327a61090e71c826/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb#L738), but we didn't apply the same logic for when we load comments lazily [here](https://github.com/Shopify/ruby-lsp/blob/9e7450dfe690a399aa25dff4327a61090e71c826/lib/ruby_indexer/lib/ruby_indexer/entry.rb#L61).

The fix is to perform the same check when lazily loading and exclude magic comments from the mix.
Hello, I would like to try and solve it if that's okay with you. This would be my first open source contribution. 
Of course. Don't hesitate to ask for help if you get stuck.



Patch:
diff --git a/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb b/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb
index 37adf9e4c..174dc346b 100644
--- a/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb
+++ b/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb
@@ -143,6 +143,7 @@ def on_singleton_class_node_enter(node)
           )
         else
           entry = Entry::SingletonClass.new(
+            @index.configuration,
             real_nesting,
             @uri,
             Location.from_prism_location(node.location, @code_units_cache),
@@ -326,6 +327,7 @@ def on_def_node_enter(node)
         signatures = [Entry::Signature.new(list_params(node.parameters))]
 
         @index.add(Entry::Method.new(
+          @index.configuration,
           method_name,
           @uri,
           location,
@@ -340,6 +342,7 @@ def on_def_node_enter(node)
           singleton = @index.existing_or_new_singleton_class(owner.name)
 
           @index.add(Entry::Method.new(
+            @index.configuration,
             method_name,
             @uri,
             location,
@@ -354,6 +357,7 @@ def on_def_node_enter(node)
         singleton = @index.existing_or_new_singleton_class(owner.name)
 
         @index.add(Entry::Method.new(
+          @index.configuration,
           method_name,
           @uri,
           Location.from_prism_location(node.location, @code_units_cache),
@@ -433,6 +437,7 @@ def on_alias_method_node_enter(node)
       comments = collect_comments(node)
       @index.add(
         Entry::UnresolvedMethodAlias.new(
+          @index.configuration,
           method_name,
           node.old_name.slice,
           @owner_stack.last,
@@ -473,6 +478,7 @@ def add_method(name, node_location, signatures, visibility: :public, comments: n
       location = Location.from_prism_location(node_location, @code_units_cache)
 
       @index.add(Entry::Method.new(
+        @index.configuration,
         name,
         @uri,
         location,
@@ -490,6 +496,7 @@ def add_module(name, full_location, name_location, comments: nil)
       name_loc = Location.from_prism_location(name_location, @code_units_cache)
 
       entry = Entry::Module.new(
+        @index.configuration,
         Index.actual_nesting(@stack, name),
         @uri,
         location,
@@ -504,6 +511,7 @@ def add_module(name, full_location, name_location, comments: nil)
     def add_class(name_or_nesting, full_location, name_location, parent_class_name: nil, comments: nil)
       nesting = name_or_nesting.is_a?(Array) ? name_or_nesting : Index.actual_nesting(@stack, name_or_nesting)
       entry = Entry::Class.new(
+        @index.configuration,
         nesting,
         @uri,
         Location.from_prism_location(full_location, @code_units_cache),
@@ -548,6 +556,7 @@ def handle_global_variable(node, loc)
       comments = collect_comments(node)
 
       @index.add(Entry::GlobalVariable.new(
+        @index.configuration,
         name,
         @uri,
         Location.from_prism_location(loc, @code_units_cache),
@@ -572,6 +581,7 @@ def handle_class_variable(node, loc)
       end
 
       @index.add(Entry::ClassVariable.new(
+        @index.configuration,
         name,
         @uri,
         Location.from_prism_location(loc, @code_units_cache),
@@ -594,6 +604,7 @@ def handle_instance_variable(node, loc)
       end
 
       @index.add(Entry::InstanceVariable.new(
+        @index.configuration,
         name,
         @uri,
         Location.from_prism_location(loc, @code_units_cache),
@@ -656,6 +667,7 @@ def handle_alias_method(node)
       comments = collect_comments(node)
       @index.add(
         Entry::UnresolvedMethodAlias.new(
+          @index.configuration,
           new_name_value,
           old_name_value,
           @owner_stack.last,
@@ -675,6 +687,7 @@ def add_constant(node, name, value = nil)
         case value
         when Prism::ConstantReadNode, Prism::ConstantPathNode
           Entry::UnresolvedConstantAlias.new(
+            @index.configuration,
             value.slice,
             @stack.dup,
             name,
@@ -688,6 +701,7 @@ def add_constant(node, name, value = nil)
           # If the right hand side is another constant assignment, we need to visit it because that constant has to be
           # indexed too
           Entry::UnresolvedConstantAlias.new(
+            @index.configuration,
             value.name.to_s,
             @stack.dup,
             name,
@@ -699,6 +713,7 @@ def add_constant(node, name, value = nil)
         Prism::ConstantPathAndWriteNode
 
           Entry::UnresolvedConstantAlias.new(
+            @index.configuration,
             value.target.slice,
             @stack.dup,
             name,
@@ -708,6 +723,7 @@ def add_constant(node, name, value = nil)
           )
         else
           Entry::Constant.new(
+            @index.configuration,
             name,
             @uri,
             Location.from_prism_location(node.location, @code_units_cache),
@@ -785,6 +801,7 @@ def handle_attribute(node, reader:, writer:)
 
         if reader
           @index.add(Entry::Accessor.new(
+            @index.configuration,
             name,
             @uri,
             Location.from_prism_location(loc, @code_units_cache),
@@ -797,6 +814,7 @@ def handle_attribute(node, reader:, writer:)
         next unless writer
 
         @index.add(Entry::Accessor.new(
+          @index.configuration,
           "#{name}=",
           @uri,
           Location.from_prism_location(loc, @code_units_cache),
@@ -879,6 +897,7 @@ def handle_module_function(node)
           singleton = @index.existing_or_new_singleton_class(entry_owner_name)
           location = Location.from_prism_location(argument.location, @code_units_cache)
           @index.add(Entry::Method.new(
+            @index.configuration,
             method_name,
             @uri,
             location,
diff --git a/lib/ruby_indexer/lib/ruby_indexer/entry.rb b/lib/ruby_indexer/lib/ruby_indexer/entry.rb
index bc595a042..e5946b9c2 100644
--- a/lib/ruby_indexer/lib/ruby_indexer/entry.rb
+++ b/lib/ruby_indexer/lib/ruby_indexer/entry.rb
@@ -3,6 +3,9 @@
 
 module RubyIndexer
   class Entry
+    #: Configuration
+    attr_reader :configuration
+
     #: String
     attr_reader :name
 
@@ -17,8 +20,9 @@ class Entry
     #: Symbol
     attr_accessor :visibility
 
-    #: (String name, URI::Generic uri, Location location, String? comments) -> void
-    def initialize(name, uri, location, comments)
+    #: (Configuration configuration, String name, URI::Generic uri, Location location, String? comments) -> void
+    def initialize(configuration, name, uri, location, comments)
+      @configuration = configuration
       @name = name
       @uri = uri
       @comments = comments
@@ -82,7 +86,7 @@ def comments
           correct_group.filter_map do |comment|
             content = comment.slice.chomp
 
-            if content.valid_encoding?
+            if content.valid_encoding? && !content.match?(@configuration.magic_comment_regex)
               content.delete_prefix!("#")
               content.delete_prefix!(" ")
               content
@@ -121,13 +125,13 @@ class Namespace < Entry
       #: Location
       attr_reader :name_location
 
-      #: (Array[String] nesting, URI::Generic uri, Location location, Location name_location, String? comments) -> void
-      def initialize(nesting, uri, location, name_location, comments)
+      #: (Configuration configuration, Array[String] nesting, URI::Generic uri, Location location, Location name_location, String? comments) -> void
+      def initialize(configuration, nesting, uri, location, name_location, comments) # rubocop:disable Metrics/ParameterLists
         @name = nesting.join("::") #: String
         # The original nesting where this namespace was discovered
         @nesting = nesting
 
-        super(@name, uri, location, comments)
+        super(configuration, @name, uri, location, comments)
 
         @name_location = name_location
       end
@@ -160,9 +164,9 @@ class Class < Namespace
       #: String?
       attr_reader :parent_class
 
-      #: (Array[String] nesting, URI::Generic uri, Location location, Location name_location, String? comments, String? parent_class) -> void
-      def initialize(nesting, uri, location, name_location, comments, parent_class) # rubocop:disable Metrics/ParameterLists
-        super(nesting, uri, location, name_location, comments)
+      #: (Configuration configuration, Array[String] nesting, URI::Generic uri, Location location, Location name_location, String? comments, String? parent_class) -> void
+      def initialize(configuration, nesting, uri, location, name_location, comments, parent_class) # rubocop:disable Metrics/ParameterLists
+        super(configuration, nesting, uri, location, name_location, comments)
         @parent_class = parent_class
       end
 
@@ -285,9 +289,9 @@ class Member < Entry
       #: Entry::Namespace?
       attr_reader :owner
 
-      #: (String name, URI::Generic uri, Location location, String? comments, Symbol visibility, Entry::Namespace? owner) -> void
-      def initialize(name, uri, location, comments, visibility, owner) # rubocop:disable Metrics/ParameterLists
-        super(name, uri, location, comments)
+      #: (Configuration configuration, String name, URI::Generic uri, Location location, String? comments, Symbol visibility, Entry::Namespace? owner) -> void
+      def initialize(configuration, name, uri, location, comments, visibility, owner) # rubocop:disable Metrics/ParameterLists
+        super(configuration, name, uri, location, comments)
         @visibility = visibility
         @owner = owner
       end
@@ -341,9 +345,9 @@ class Method < Member
       #: Location
       attr_reader :name_location
 
-      #: (String name, URI::Generic uri, Location location, Location name_location, String? comments, Array[Signature] signatures, Symbol visibility, Entry::Namespace? owner) -> void
-      def initialize(name, uri, location, name_location, comments, signatures, visibility, owner) # rubocop:disable Metrics/ParameterLists
-        super(name, uri, location, comments, visibility, owner)
+      #: (Configuration configuration, String name, URI::Generic uri, Location location, Location name_location, String? comments, Array[Signature] signatures, Symbol visibility, Entry::Namespace? owner) -> void
+      def initialize(configuration, name, uri, location, name_location, comments, signatures, visibility, owner) # rubocop:disable Metrics/ParameterLists
+        super(configuration, name, uri, location, comments, visibility, owner)
         @signatures = signatures
         @name_location = name_location
       end
@@ -366,9 +370,9 @@ class UnresolvedConstantAlias < Entry
       #: Array[String]
       attr_reader :nesting
 
-      #: (String target, Array[String] nesting, String name, URI::Generic uri, Location location, String? comments) -> void
-      def initialize(target, nesting, name, uri, location, comments) # rubocop:disable Metrics/ParameterLists
-        super(name, uri, location, comments)
+      #: (Configuration configuration, String target, Array[String] nesting, String name, URI::Generic uri, Location location, String? comments) -> void
+      def initialize(configuration, target, nesting, name, uri, location, comments) # rubocop:disable Metrics/ParameterLists
+        super(configuration, name, uri, location, comments)
 
         @target = target
         @nesting = nesting
@@ -383,6 +387,7 @@ class ConstantAlias < Entry
       #: (String target, UnresolvedConstantAlias unresolved_alias) -> void
       def initialize(target, unresolved_alias)
         super(
+          unresolved_alias.configuration,
           unresolved_alias.name,
           unresolved_alias.uri,
           unresolved_alias.location,
@@ -402,9 +407,9 @@ class ClassVariable < Entry
       #: Entry::Namespace?
       attr_reader :owner
 
-      #: (String name, URI::Generic uri, Location location, String? comments, Entry::Namespace? owner) -> void
-      def initialize(name, uri, location, comments, owner)
-        super(name, uri, location, comments)
+      #: (Configuration configuration, String name, URI::Generic uri, Location location, String? comments, Entry::Namespace? owner) -> void
+      def initialize(configuration, name, uri, location, comments, owner) # rubocop:disable Metrics/ParameterLists
+        super(configuration, name, uri, location, comments)
         @owner = owner
       end
     end
@@ -414,9 +419,9 @@ class InstanceVariable < Entry
       #: Entry::Namespace?
       attr_reader :owner
 
-      #: (String name, URI::Generic uri, Location location, String? comments, Entry::Namespace? owner) -> void
-      def initialize(name, uri, location, comments, owner)
-        super(name, uri, location, comments)
+      #: (Configuration configuration, String name, URI::Generic uri, Location location, String? comments, Entry::Namespace? owner) -> void
+      def initialize(configuration, name, uri, location, comments, owner) # rubocop:disable Metrics/ParameterLists
+        super(configuration, name, uri, location, comments)
         @owner = owner
       end
     end
@@ -431,9 +436,9 @@ class UnresolvedMethodAlias < Entry
       #: Entry::Namespace?
       attr_reader :owner
 
-      #: (String new_name, String old_name, Entry::Namespace? owner, URI::Generic uri, Location location, String? comments) -> void
-      def initialize(new_name, old_name, owner, uri, location, comments) # rubocop:disable Metrics/ParameterLists
-        super(new_name, uri, location, comments)
+      #: (Configuration configuration, String new_name, String old_name, Entry::Namespace? owner, URI::Generic uri, Location location, String? comments) -> void
+      def initialize(configuration, new_name, old_name, owner, uri, location, comments) # rubocop:disable Metrics/ParameterLists
+        super(configuration, new_name, uri, location, comments)
 
         @new_name = new_name
         @old_name = old_name
@@ -456,6 +461,7 @@ def initialize(target, unresolved_alias)
         full_comments << target.comments
 
         super(
+          unresolved_alias.configuration,
           unresolved_alias.new_name,
           unresolved_alias.uri,
           unresolved_alias.location,
diff --git a/lib/ruby_indexer/lib/ruby_indexer/index.rb b/lib/ruby_indexer/lib/ruby_indexer/index.rb
index 28293bf31..4cf8895d6 100644
--- a/lib/ruby_indexer/lib/ruby_indexer/index.rb
+++ b/lib/ruby_indexer/lib/ruby_indexer/index.rb
@@ -714,6 +714,7 @@ def existing_or_new_singleton_class(name)
         attached_ancestor = self[name]&.first #: as !nil
 
         singleton = Entry::SingletonClass.new(
+          @configuration,
           [full_singleton_name],
           attached_ancestor.uri,
           attached_ancestor.location,
diff --git a/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb b/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb
index 3cb1eddb5..2f3922bb8 100644
--- a/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb
+++ b/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb
@@ -52,9 +52,9 @@ def handle_class_or_module_declaration(declaration, pathname)
       comments = comments_to_string(declaration)
       entry = if declaration.is_a?(RBS::AST::Declarations::Class)
         parent_class = declaration.super_class&.name&.name&.to_s
-        Entry::Class.new(nesting, uri, location, location, comments, parent_class)
+        Entry::Class.new(@index.configuration, nesting, uri, location, location, comments, parent_class)
       else
-        Entry::Module.new(nesting, uri, location, location, comments)
+        Entry::Module.new(@index.configuration, nesting, uri, location, location, comments)
       end
 
       add_declaration_mixins_to_entry(declaration, entry)
@@ -110,6 +110,7 @@ def handle_method(member, owner)
       real_owner = member.singleton? ? @index.existing_or_new_singleton_class(owner.name) : owner
       signatures = signatures(member)
       @index.add(Entry::Method.new(
+        @index.configuration,
         name,
         uri,
         location,
@@ -243,6 +244,7 @@ def process_rest_keywords(function)
     def handle_constant(declaration, nesting, uri)
       fully_qualified_name = [*nesting, declaration.name.name.to_s].join("::")
       @index.add(Entry::Constant.new(
+        @index.configuration,
         fully_qualified_name,
         uri,
         to_ruby_indexer_location(declaration.location),
@@ -258,6 +260,7 @@ def handle_global_variable(declaration, pathname)
       comments = comments_to_string(declaration)
 
       @index.add(Entry::GlobalVariable.new(
+        @index.configuration,
         name,
         uri,
         location,
@@ -271,6 +274,7 @@ def handle_signature_alias(member, owner_entry)
       comments = comments_to_string(member)
 
       entry = Entry::UnresolvedMethodAlias.new(
+        @index.configuration,
         member.new_name.to_s,
         member.old_name.to_s,
         owner_entry,


Test patch:
diff --git a/lib/ruby_indexer/test/classes_and_modules_test.rb b/lib/ruby_indexer/test/classes_and_modules_test.rb
index 6a0ed097e..349465921 100644
--- a/lib/ruby_indexer/test/classes_and_modules_test.rb
+++ b/lib/ruby_indexer/test/classes_and_modules_test.rb
@@ -766,5 +766,25 @@ class Foo
         FileUtils.rm(path)
       end
     end
+
+    def test_lazy_comments_ignores_magic_comments
+      path = File.join(Dir.pwd, "lib", "foo.rb")
+      source = <<~RUBY
+        # frozen_string_literal: true
+
+        class Foo
+        end
+      RUBY
+      File.write(path, source)
+      @index.index_single(URI::Generic.from_path(path: path), source, collect_comments: false)
+
+      entry = @index["Foo"]&.first #: as !nil
+
+      begin
+        assert_empty(entry.comments)
+      ensure
+        FileUtils.rm(path)
+      end
+    end
   end
 end