Conversation
ComplexType#intersect_with keeps a declared union member only when it conforms to a guard type or the guard type conforms to it. A member related to the guard in neither direction falls through both branches of that test and is dropped, so narrowing 'A, B_with_M' by an is_a?(M) guard yields 'B_with_M' alone. That is narrower than reality: an A_with_M satisfies the declared A, passes the guard, and is no longer admitted. The first example asserts that soundness property rather than an exact tag string, so it goes green under any correct answer. The second pins the precise result and stays pending, because the sound answer 'A & M, B_with_M' needs intersection types that master cannot represent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EiauH8LL9NaN6hbx5EaNbB
ComplexType#intersect_with kept a declared union member only when it conformed to the guard type or the guard conformed to it, and dropped it otherwise. Dropping is correct for two classes, which single inheritance proves cannot overlap, and narrowing a union by is_a? depends on it. It is wrong when either side is a module, because a subclass can mix the module in, so no such pair can be ruled out. Narrowing [A, B_with_M] by is_a?(M) therefore inferred B_with_M alone, excluding an A_with_M that satisfies the declared A, passes the guard, and is returned. The mirror case -- a module member against a class guard -- dropped every member and fell back to undefined, silencing the check rather than narrowing it. Both copies now keep the guard's type when neither side conforms and either is a module. That is the closest sound answer expressible here; the exact one is the two intersected, which has no representation yet. ApiMap#module? wraps the existing private get_namespace_type so the conformance code does not reimplement it at the call site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EiauH8LL9NaN6hbx5EaNbB
Lint/DuplicateBranch flags the first and last branches of both intersect_with chains, which do share a body. They do not share a meaning, and in ComplexType#intersect_with they cannot be merged: the module test has to stay last. Hoisted into the first branch, any pair where ut conforms to int_type and either side is a module changes answer - String against a Comparable guard keeps String today and would start keeping Comparable. UniqueType#intersect_with tests the two directions in the opposite order, so merging there would in fact be safe. It keeps the same shape anyway: the two are sibling copies that have to stay in step, and a shape that differs between them invites drift. Also adds the blank line RSpec/EmptyLineAfterExampleGroup wants. Verified with an explicit config, which stops RuboCop searching upward into the primary checkout: bundle exec rubocop -c .rubocop.yml --only Lint/DuplicateBranch,RSpec/EmptyLineAfterExampleGroup reports no offenses across the three files.
undercover reported lib/solargraph/complex_type/unique_type.rb:133:151 at 0.0% coverage: the existing specs drive ComplexType#intersect_with, and the UniqueType copy was reached by nothing. That matters more than an ordinary coverage gap here. The two are sibling copies rather than an override, they test the conformance directions in opposite order, and any change has to be written twice - an untested copy is how the pair drifts apart. Three examples on the UniqueType receiver: the module case that keeps the guard, the case where the declared type already implies the guard and keeps itself, and the soundness assertion that a value satisfying both is still admitted.
Owner
Author
|
Claude: Reopened upstream as castwide#1361 with the same branch, title and description. Closing this one so the change has a single home. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was written by Claude Code on behalf of @apiology.
Problem: When a guard tests for a module, a union member not already known to include it is dropped — so the inferred type omits a value that can actually occur.
A_with_Msatisfies the declaredA, passes the guard, and is returned, yet the inferred type admits onlyB_with_M. The mirror case — a module member against a class guard — is quieter still: every member drops, the empty result falls back toundefined, and the file reports no problems at all.Solution: Keep the guard's type when the two types are related in neither direction and either side is a module, since a subclass can always mix a module in. Two unrelated classes remain disjoint under single inheritance and are still dropped.
The exact answer is their intersection, which cannot be spelled yet; a pending spec asserts it against castwide#1231.
Test plan:
origin/masterbaseline: 531 problems in 90 of 250 files on both sides, problem lists identical after normalizing paths and line numbers.