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.
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?orreturn ... if x.nil?(or theunlessinverse), after whichxis used either as a call argument or as the receiver of a call. Atstronglevel,xcontinues to report as nilable in both cases, even though the guard has already eliminatednilfor the rest of the method.This is a different, narrower-scoped gap than the already-filed flow-sensitivity issues:
case/whensubject narrowingis_a?checks combined with&&or insideelsifbranch bodiesNone 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
Both
processandlength_ofreport a type mismatch on the guarded variable despite the preceding guard making that case unreachable.Context
Found while auditing
@sg-ignoreaccumulation in a downstream app (apiology/plate-spinner) — this single pattern accounts for 15 of the app's@sg-ignoremarkers across 10 files, split roughly evenly between the argument-position and receiver-position shapes above. It's also not reliably escapable with the usual@typecast workaround: at least 2 of those 15 occurrences still report the mismatch even after adding an explicit@typecast on a local immediately following the guard.