Skip to content

Flow-sensitive typing doesn't narrow past a raise/return-based nil guard #1254

Description

@apiology

Claude:

Summary

Flow-sensitive typing does not narrow a variable's type past a raise/return-based nil guard — i.e. raise "..." if x.nil? or return ... if x.nil? (or the unless inverse), after which x is used either as a call argument or as the receiver of a call. At strong level, x continues to report as nilable in both cases, even though the guard has already eliminated nil for the rest of the method.

This is a different, narrower-scoped gap than the already-filed flow-sensitivity issues:

None of those cover a raise/return-based early-exit guard specifically. It's also the pattern named but not individually tracked in #1240's description ("flow-sensitive-typing limits (postfix nil guards, ...)").

Repro

# typecheck --level strong

# @param x [String, nil]
# @return [void]
def process(x)
  raise 'x required' if x.nil?

  # `x` is still typed as `String, nil` here, not narrowed to `String` -
  # passing it to a String-only parameter is flagged even though the
  # raise above already eliminates the nil case for the rest of the method:
  accepts_string(x)
end

# @param s [String]
# @return [void]
def accepts_string(s)
  puts s
end

# @param x [String, nil]
# @return [Integer]
def length_of(x)
  return 0 if x.nil?

  # Same gap when `x` is used as a call *receiver* instead of an argument -
  # still typed as `String, nil` here too:
  x.length
end

Both process and length_of report a type mismatch on the guarded variable despite the preceding guard making that case unreachable.

Context

Found while auditing @sg-ignore accumulation in a downstream app (apiology/plate-spinner) — this single pattern accounts for 15 of the app's @sg-ignore markers across 10 files, split roughly evenly between the argument-position and receiver-position shapes above. It's also not reliably escapable with the usual @type cast workaround: at least 2 of those 15 occurrences still report the mismatch even after adding an explicit @type cast on a local immediately following the guard.

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