Repository: ruby/rbs (Ruby)

Issue:
RBS allows including/prepending/extending with a class constant instead of module constant
It seems that RBS allows including/prepending/extending a class constant. This is a surprise to me as normally it's only possible to include modules into classes/modules in ruby, and I would have expected the same in RBS.

```
class Foo
  def foo: () -> untyped
end

class Qux
  include Foo
end
```

`rbs methods Qux | grep foo` -> `foo (public)`

I would expect classes/modules to not be allowed to include/prepend/extend other classes, and for a sensible error message to be shown. At the moment `steep check` exits with an exception, but it is not a helpful error message.


Discussion:

Patch:
diff --git a/lib/rbs/definition_builder/ancestor_builder.rb b/lib/rbs/definition_builder/ancestor_builder.rb
index 36a0957f6..a2597fd27 100644
--- a/lib/rbs/definition_builder/ancestor_builder.rb
+++ b/lib/rbs/definition_builder/ancestor_builder.rb
@@ -237,6 +237,7 @@ def one_instance_ancestors(type_name)
         end
 
         mixin_ancestors(entry,
+                        type_name,
                         included_modules: ancestors.included_modules,
                         included_interfaces: ancestors.included_interfaces,
                         prepended_modules: ancestors.prepended_modules,
@@ -284,6 +285,7 @@ def one_singleton_ancestors(type_name)
         end
 
         mixin_ancestors(entry,
+                        type_name,
                         included_modules: nil,
                         included_interfaces: nil,
                         prepended_modules: nil,
@@ -301,6 +303,7 @@ def one_interface_ancestors(type_name)
 
             OneAncestors.interface(type_name: type_name, params: params).tap do |ancestors|
               mixin_ancestors0(entry.decl,
+                               type_name,
                                align_params: nil,
                                included_modules: nil,
                                included_interfaces: ancestors.included_interfaces,
@@ -311,7 +314,7 @@ def one_interface_ancestors(type_name)
           end
       end
 
-      def mixin_ancestors0(decl, align_params:, included_modules:, included_interfaces:, extended_modules:, prepended_modules:, extended_interfaces:)
+      def mixin_ancestors0(decl, type_name, align_params:, included_modules:, included_interfaces:, extended_modules:, prepended_modules:, extended_interfaces:)
         decl.each_mixin do |member|
           case member
           when AST::Members::Include
@@ -321,6 +324,7 @@ def mixin_ancestors0(decl, align_params:, included_modules:, included_interfaces
 
             case
             when member.name.class? && included_modules
+              MixinClassError.check!(type_name: type_name, env: env, member: member)
               NoMixinFoundError.check!(member.name, env: env, member: member)
               included_modules << ancestor
             when member.name.interface? && included_interfaces
@@ -330,6 +334,7 @@ def mixin_ancestors0(decl, align_params:, included_modules:, included_interfaces
 
           when AST::Members::Prepend
             if prepended_modules
+              MixinClassError.check!(type_name: type_name, env: env, member: member)
               NoMixinFoundError.check!(member.name, env: env, member: member)
 
               module_name = member.name
@@ -345,6 +350,7 @@ def mixin_ancestors0(decl, align_params:, included_modules:, included_interfaces
 
             case
             when member.name.class? && extended_modules
+              MixinClassError.check!(type_name: type_name, env: env, member: member)
               NoMixinFoundError.check!(member.name, env: env, member: member)
               extended_modules << ancestor
             when member.name.interface? && extended_interfaces
@@ -355,7 +361,7 @@ def mixin_ancestors0(decl, align_params:, included_modules:, included_interfaces
         end
       end
 
-      def mixin_ancestors(entry, included_modules:, included_interfaces:, extended_modules:, prepended_modules:, extended_interfaces:)
+      def mixin_ancestors(entry, type_name, included_modules:, included_interfaces:, extended_modules:, prepended_modules:, extended_interfaces:)
         entry.decls.each do |d|
           decl = d.decl
 
@@ -365,6 +371,7 @@ def mixin_ancestors(entry, included_modules:, included_interfaces:, extended_mod
           )
 
           mixin_ancestors0(decl,
+                           type_name,
                            align_params: align_params,
                            included_modules: included_modules,
                            included_interfaces: included_interfaces,
diff --git a/lib/rbs/errors.rb b/lib/rbs/errors.rb
index 0b6d0bdf9..1dd40d2ba 100644
--- a/lib/rbs/errors.rb
+++ b/lib/rbs/errors.rb
@@ -350,4 +350,40 @@ def location
       original.location
     end
   end
+
+  class MixinClassError < DefinitionError
+    attr_reader :type_name
+    attr_reader :member
+
+    def initialize(type_name:, member:)
+      @type_name = type_name
+      @member = member
+
+      super "#{Location.to_string member.location}: Cannot #{mixin_name} a class `#{member.name}` in the definition of `#{type_name}`"
+    end
+
+    def location
+      member.location
+    end
+
+    def self.check!(type_name:, env:, member:)
+      case env.class_decls[member.name]
+      when Environment::ClassEntry
+        raise new(type_name: type_name, member: member)
+      end
+    end
+
+    private
+
+    def mixin_name
+      case member
+      when AST::Members::Prepend
+        "prepend"
+      when AST::Members::Include
+        "include"
+      when AST::Members::Extend
+        "extend"
+      end
+    end
+  end
 end
diff --git a/sig/ancestor_builder.rbs b/sig/ancestor_builder.rbs
index e2ed3b09d..7e9b3ea9b 100644
--- a/sig/ancestor_builder.rbs
+++ b/sig/ancestor_builder.rbs
@@ -80,6 +80,7 @@ module RBS
       def interface_ancestors: (TypeName, ?building_ancestors: Array[Definition::Ancestor::t]) -> Definition::InstanceAncestors
 
       def mixin_ancestors: (Environment::ClassEntry | Environment::ModuleEntry,
+                            TypeName,
                             included_modules: Array[Definition::Ancestor::Instance]?,
                          included_interfaces:Array[Definition::Ancestor::Instance]?,
                            prepended_modules: Array[Definition::Ancestor::Instance]?,
@@ -87,6 +88,7 @@ module RBS
                          extended_interfaces: Array[Definition::Ancestor::Instance]?) -> void
 
       def mixin_ancestors0: (AST::Declarations::Class | AST::Declarations::Module | AST::Declarations::Interface,
+                             TypeName,
                              align_params: Substitution?,
                          included_modules: Array[Definition::Ancestor::Instance]?,
                       included_interfaces:Array[Definition::Ancestor::Instance]?,
diff --git a/sig/errors.rbs b/sig/errors.rbs
index a4b3b5f9d..57db71c0b 100644
--- a/sig/errors.rbs
+++ b/sig/errors.rbs
@@ -181,4 +181,19 @@ module RBS
 
     def location: () -> Location?
   end
+
+  class MixinClassError < DefinitionError
+    type member = AST::Members::Include | AST::Members::Prepend | AST::Members::Extend
+
+    attr_reader type_name: TypeName
+    attr_reader member: member
+
+    def initialize: (type_name: TypeName, member: member) -> void
+
+    def location: () -> Location?
+
+    def self.check!: (type_name: TypeName, env: Environment, member: member) -> void
+
+    def mixin_name: () -> String
+  end
 end


Test patch:
diff --git a/test/rbs/ancestor_builder_test.rb b/test/rbs/ancestor_builder_test.rb
index 59a8eb9f8..755ff38ac 100644
--- a/test/rbs/ancestor_builder_test.rb
+++ b/test/rbs/ancestor_builder_test.rb
@@ -14,6 +14,7 @@ class RBS::AncestorBuilderTest < Test::Unit::TestCase
   InvalidTypeApplicationError = RBS::InvalidTypeApplicationError
   RecursiveAncestorError = RBS::RecursiveAncestorError
   SuperclassMismatchError = RBS::SuperclassMismatchError
+  MixinClassError = RBS::MixinClassError
 
   def test_one_ancestors_class
     SignatureManager.new(system_builtin: true) do |manager|
@@ -506,4 +507,73 @@ class C
       end
     end
   end
+
+  def test_invalid_mixin_include
+    SignatureManager.new do |manager|
+      manager.files[Pathname("foo.rbs")] = <<EOF
+class Foo
+  def foo: () -> untyped
+end
+
+class Qux
+  include Foo
+end
+EOF
+      manager.build do |env|
+        builder = DefinitionBuilder::AncestorBuilder.new(env: env)
+
+        assert_raises MixinClassError do
+          builder.instance_ancestors(type_name("::Qux"))
+        end
+      end
+    end
+  end
+
+  def test_invalid_mixin_perpend
+    SignatureManager.new do |manager|
+      manager.files[Pathname("foo.rbs")] = <<EOF
+class Foo
+  def foo: () -> untyped
+end
+
+class Qux
+  prepend Foo
+end
+EOF
+
+  manager.build do |env|
+        manager.build do |env|
+          builder = DefinitionBuilder::AncestorBuilder.new(env: env)
+
+          assert_raises MixinClassError do
+            builder.instance_ancestors(type_name("::Qux"))
+          end
+        end
+      end
+    end
+  end
+
+  def test_invalid_mixin_extend
+    SignatureManager.new do |manager|
+      manager.files[Pathname("foo.rbs")] = <<EOF
+class Foo
+  def foo: () -> untyped
+end
+
+class Qux
+  extend Foo
+end
+EOF
+
+  manager.build do |env|
+        manager.build do |env|
+          builder = DefinitionBuilder::AncestorBuilder.new(env: env)
+
+          assert_raises MixinClassError do
+            builder.one_singleton_ancestors(type_name("::Qux"))
+          end
+        end
+      end
+    end
+  end
 end