Summary
Flow-sensitive typing narrows nil-checks on local variables (x = foo; return if x.nil?; x.bar), but does not narrow repeated calls to the same no-argument accessor method (e.g. an attr_reader) even when the method is a pure getter with no side effects. A nil-guard on obj.attr does not narrow a later call to obj.attr in the same expression or method body — each call is treated as an independent, unnarrowed invocation.
Repro
# typecheck --level strong
class Location
# @return [String]
def filename; end
end
class Pin
# @return [Location, nil]
attr_reader :location
end
# @param pin [Pin]
def bundled_filename(pin)
# `pin.location` is nil-guarded here...
return nil unless pin.location
# ...but this second call to `pin.location` is still typed
# `Location, nil`, so `.filename` is flagged as a possible nil
# dereference even though the guard above already ruled that out.
pin.location.filename
end
Typechecking this at strong reports a problem on pin.location.filename, even though pin.location was already confirmed non-nil on the line above.
Context
Found while auditing @sg-ignore accumulation in #1240 (strong-level typecheck cleanup) — this is the largest single category there (30 occurrences), spread across pin/base.rb, pin/block.rb, pin/callable.rb, type_checker.rb, and others. Representative real examples:
# lib/solargraph/pin/base.rb
def filename
return nil if location.nil?
# @sg-ignore flow sensitive typing needs to handle attrs
location.filename
end
# lib/solargraph/type_checker.rb
def internal? pin
return false if pin.nil?
# @sg-ignore flow sensitive typing needs to handle attrs
pin.location && api_map.bundled?(pin.location.filename)
end
A reasonable fix scope: when a method is a simple, argument-free attr_reader-style accessor (or otherwise provably pure/idempotent), treat repeated calls to it within the same flow-sensitive-typing scope the same way a local variable holding its result would be treated for nil-narrowing purposes.
Summary
Flow-sensitive typing narrows nil-checks on local variables (
x = foo; return if x.nil?; x.bar), but does not narrow repeated calls to the same no-argument accessor method (e.g. anattr_reader) even when the method is a pure getter with no side effects. A nil-guard onobj.attrdoes not narrow a later call toobj.attrin the same expression or method body — each call is treated as an independent, unnarrowed invocation.Repro
Typechecking this at
strongreports a problem onpin.location.filename, even thoughpin.locationwas already confirmed non-nil on the line above.Context
Found while auditing
@sg-ignoreaccumulation in #1240 (strong-level typecheck cleanup) — this is the largest single category there (30 occurrences), spread acrosspin/base.rb,pin/block.rb,pin/callable.rb,type_checker.rb, and others. Representative real examples:A reasonable fix scope: when a method is a simple, argument-free
attr_reader-style accessor (or otherwise provably pure/idempotent), treat repeated calls to it within the same flow-sensitive-typing scope the same way a local variable holding its result would be treated for nil-narrowing purposes.