Skip to content

Narrow is_a? checks joined by || to their union type - #64

Draft
apiology wants to merge 1 commit into
masterfrom
fix-isa-or-narrowing
Draft

Narrow is_a? checks joined by || to their union type#64
apiology wants to merge 1 commit into
masterfrom
fix-isa-or-narrowing

Conversation

@apiology

Copy link
Copy Markdown
Owner

This PR was written by Claude Code on behalf of @apiology.

FlowSensitiveTyping#process_or never computed any true-branch (positive) narrowing for an ||-joined condition — its own comment notes "can't assume if an or is true that every single condition is true, so don't provide true ranges to assert facts on." That's correct in general, but it means if e.is_a?(Array) || e.is_a?(String) never narrows e inside the if body at all, even though the disjunction does guarantee e is one of Array | String there.

Example that fails solargraph typecheck --level strong on master:

class Repro
  # @param e [Array<Symbol>, String]
  def eval_function_call(e); end

  # @param e [Array<Symbol>, String, Integer]
  def eval(e)
    if e.is_a?(Array) || e.is_a?(String)
      eval_function_call(e)  # reports e as Array<Symbol>, String, Integer — the full param type, unnarrowed
    end
  end
end

This adds narrowing for the one case that's sound to infer generically: both sides of the || are plain is_a? calls on the same variable. In that case the variable is narrowed to the union of the two checked types on the true branch. Any other shape — different variables on each side, or either side not a plain is_a? call — is left exactly as before (no narrowing), rather than guessing.

Known limitation: a chain of three or more is_a? checks (a.is_a?(X) || a.is_a?(Y) || a.is_a?(Z)) still gets no narrowing, because the AST nests as (X || Y) || Z and the outer left-hand side is an :or node, not a :send, so parse_isa returns nil on it. This degrades to today's (no-narrowing) behavior rather than narrowing incorrectly, but a future PR could recurse to handle it.

Adds three specs to spec/parser/flow_sensitive_typing_spec.rb: the positive union case, a negative control (single is_a?, unaffected), and a soundness control (different variables on each side of the ||, still unnarrowed).

FlowSensitiveTyping#process_or never computed any true-branch
narrowing for an ||-joined condition, so a check like
e.is_a?(Array) || e.is_a?(String) left e at its full declared type
inside the if body instead of narrowing to Array | String.

Add process_or_isa_union, which narrows only when both sides of the
|| are plain is_a? calls on the same variable, using the same
process_facts/find_var/ComplexType.parse primitives process_isa
already uses. Any other shape (different variables, or a side that
isn't a plain is_a? call) is left unnarrowed rather than guessed at.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant