Skip to content

Flow-sensitive typing doesn't narrow repeated calls to the same attr_reader-style method #1249

Description

@apiology

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.

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