Skip to content

RBS intersection types (&) parse but are translated to union semantics, not intersection semanticsΒ #1229

Description

@apiology

πŸ€– Filed by Claude, not the account owner β€” acting on their behalf via their GitHub credentials.

Summary

RBS type syntax supports intersection types (A & B, see RBS syntax docs), and Solargraph parses & in inline RBS method signatures (#1173) without error. But the semantics aren't implemented: RbsTranslator#type_to_tag converts RBS::Types::Intersection the same way it converts RBS::Types::Union β€” by joining member types with , :

https://github.com/castwide/solargraph/blob/v0.60.2/lib/solargraph/rbs_translator.rb#L154-L155

when RBS::Types::Intersection
  type.types.map { |member| type_to_tag(member) }.join(', ')

Since Solargraph::ComplexType has no internal representation for intersections (only comma-separated unions), A & B ends up behaving exactly like the union (A, B) once translated β€” assignable only where every member type would independently be accepted, rather than assignable anywhere any one member type is expected (the actual subtyping rule for intersections: A & B <: A and A & B <: B).

Reproduction

class Consumer
  # @param project_obj [Asana::Resources::Project]
  # @return [void]
  def project_to_h(project_obj); end
end

class MockFactory
  #: () -> (Mocha::Mock & Asana::Resources::Project)
  def make_mock
    Mocha::Mock.new
  end
end

Consumer.new.project_to_h(MockFactory.new.make_mock)

solargraph typecheck --level strong reports:

Wrong argument type for Consumer#project_to_h: project_obj expected Asana::Resources::Project, received Mocha::Mock, Asana::Resources::Project

The "received Mocha::Mock, Asana::Resources::Project" is the union tag β€” proof the intersection collapsed into a union rather than being checked as a true intersection (which would have passed silently, since the argument's declared type includes Asana::Resources::Project as one conjunct).

Motivating use case

This came up trying to type Minitest+Mocha test doubles precisely: a Mocha::Mock configured via responds_like_instance_of(RealClass) duck-types correctly as RealClass at every call site the code under test exercises, but is statically only a Mocha::Mock. An intersection type would let call sites pass such doubles to methods declared @param x [RealClass] without widening to Object, adding a misleading union, or suppressing with @sg-ignore per call site (see apiology/checkoff#462, e.g. this thread, which hit this ~15-20 times).

What's needed

RbsTranslator (and/or ComplexType) needs an actual intersection representation, distinct from union, so type-compatibility checks honor A & B <: A / A & B <: B rather than flattening to a union tag.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions