Skip to content

Narrow bare, implicit-self attr_reader-style accessor calls - #53

Open
apiology wants to merge 2 commits into
gh-fix-1249from
self-rooted-accessor-narrowing
Open

Narrow bare, implicit-self attr_reader-style accessor calls#53
apiology wants to merge 2 commits into
gh-fix-1249from
self-rooted-accessor-narrowing

Conversation

@apiology

@apiology apiology commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

A nil-guard on a bare, implicit-self call (e.g. return nil if steps.nil?, where steps is an argless method like an attr_reader) left a later call to the same accessor (e.g. steps.empty?) unnarrowed — reported at castwide#1258 (comment).

Root cause: FlowSensitiveTyping#chain_pin's length-1 branch always went through find_var, which only matches tracked local/instance-variable pins. A bare steps reference is a :send node (method call), not :lvar, so lookup silently found nothing.

class Repro
  # @return [Array<Hash>, nil]
  attr_reader :steps

  def identify
    return nil if steps.nil? # or: return nil unless steps
    steps.empty? # was: Unresolved call to empty? on Array<Hash>, nil
  end
end

chain_pin now distinguishes a length-1 chain word rooted in a real :lvar/:ivar node from one rooted in a :send node (a 0-arg self-call), and synthesizes a pin against the enclosing closure (now threaded into FlowSensitiveTyping from each node processor's region.closure) instead of a variable's. process_call_chain's bare-truthy-guard handling is extended from chain length >= 2 to >= 1 for the same reason.

Based on gh-fix-1249 (castwide/solargraph#1258) since it depends on that PR's chain-narrowing infrastructure. Once castwide/solargraph#1258 merges, this should be retargeted at castwide/solargraph:master (or opened fresh there).

Test plan

  • Added 2 regression specs (bare .nil? guard, bare truthy guard) to spec/parser/flow_sensitive_typing_spec.rb
  • bundle exec rspec -- 1629 examples, 0 failures
  • bundle exec rubocop on changed files -- no offenses
  • bundle exec solargraph typecheck --level strong on changed files -- 28 pre-existing problems on gh-fix-1249 unmodified; this diff adds none (verified line-by-line)
  • Manually reproduced the original comment's repro and confirmed it now type-checks clean

🤖 Generated with Claude Code

https://claude.ai/code/session_01PMbuVPLPj8CjHG8EkEQjrh

A nil-guard on a bare call (e.g. 'return nil if steps.nil?', where
steps is an argless method like an attr_reader) previously left a
later call to the same accessor (e.g. 'steps.empty?') unnarrowed --
FlowSensitiveTyping's chain-narrowing (added for explicit-receiver
chains like 'pin.location') only resolved a single-word chain via
find_var, which looks up tracked local/instance variables and can
never match a method call.

chain_pin now recognizes when a length-1 chain word actually came
from a :send node (a method call, since the parser only emits :lvar
for names already assigned as locals in scope) rather than an :lvar
node, and synthesizes a pin rooted at the enclosing closure instead
of a variable's. FlowSensitiveTyping now takes that closure as a
constructor argument from each node processor's `region.closure`.

process_call_chain's bare-truthy-check handling is extended from
chain_words.length >= 2 to length >= 1 for the same reason, so
'return nil unless steps' narrows the same way 'return nil if
steps.nil?' does.

SKIP=Solargraph: this branch (castwide#1258, unmerged)
already has 28 pre-existing `solargraph typecheck --level strong`
problems in these files before this commit; this change adds none
(verified line-by-line against the pre-existing baseline).

Fixes castwide#1258 (comment)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMbuVPLPj8CjHG8EkEQjrh
apiology added a commit that referenced this pull request Aug 6, 2026
apiology added a commit that referenced this pull request Aug 6, 2026
#53 added a required closure parameter to
FlowSensitiveTyping#initialize and updated every caller it knew about -
but its branch is based on castwide#1258, not
castwide#1259 (already merged into this integration branch
separately), so it never saw case_node.rb (added by castwide#1259) or the
already-existing call in orasgn_node.rb that castwide#1259 also touches.

Both call sites already had region.closure in scope; added it as the
5th argument, matching every other already-updated caller
(and_node.rb, if_node.rb, or_node.rb, while_node.rb).

Verified: spec/parser/flow_sensitive_typing_spec.rb (65 examples),
spec/parser (323 examples), and spec/source_map/clip_spec.rb all pass
locally with 0 failures.
@apiology
apiology marked this pull request as ready for review August 6, 2026 02:15
@apiology

apiology commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Claude: drafted while auditing @sg-ignore suppressions in a downstream project, reviewing before posting.

This fix narrows a bare accessor when the guarded expression is called again directly, but the narrowing doesn't propagate through an assignment into a fresh local.

Reproduction

# typed: true
# frozen_string_literal: true

class Repro
  # @return [Array<Hash>, nil]
  attr_reader :steps

  # @return [Boolean]
  def works
    return false if steps.nil?

    steps.empty?  # 0 problems - narrows correctly
  end

  # @return [Boolean]
  def still_broken
    return false if steps.nil?

    local = steps
    local.empty?  # Unresolved call to empty?
  end
end

Confirmed against the current fork tip (this PR's commit is already an ancestor).

…nment

#53 (comment) reported that narrowing a bare,
implicit-self accessor doesn't propagate through an assignment into a
fresh local (e.g. local = steps; local.empty? left unresolved).

Verified against this branch's HEAD (bbaf7f9) that the reported
repro already resolves correctly: solargraph typecheck --level
strong reports 0 problems, and Chain#infer for local.empty?
resolves local to ::Array<::Hash>. BaseVariable#probe re-infers a
local's assignment expression at its own source position via
Chain#infer, and that position already falls inside the narrowed
presence range recorded by FlowSensitiveTyping for the bare steps
call, so the fact carries through without any code change needed.

This adds the missing spec coverage for that path so a future
regression here is caught.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiL3AMsRKcUzfrTHTsVzxv
@apiology

apiology commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Reopening to nudge CI ��� push at 2026-08-06T19:01:06Z never triggered a synchronize event (no check-runs created for commit 70992a5 after 20+ minutes, while other branches in this repo triggered CI within seconds).

@apiology apiology closed this Aug 6, 2026
@apiology apiology reopened this Aug 6, 2026
@apiology

apiology commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Claude: investigated this report.

Doesn't reproduce at this branch's current HEAD, commit bbaf7f9.

Checked two ways:

  1. solargraph typecheck --level strong on the exact repro from this comment (both works and still_broken) reports 0 problems, including a fresh (non-cached) run.
  2. Direct ApiMap#clip_at probe on local.empty? infers local as ::Array<::Hash>.

Root cause it isn't hitting: BaseVariable#probe re-infers a local's assignment expression (Chain#infer) at the assignment's own source position, not at the point of use. local = steps sits inside the presence range FlowSensitiveTyping already recorded for the bare steps call from the .nil? guard, so the narrowing fact is already in scope when local's type gets computed — no propagation-through-assignment logic is needed for this shape.

Added a spec (narrows a bare, implicit-self attr_reader-style accessor assigned into a fresh local variable) matching this comment's repro to spec/parser/flow_sensitive_typing_spec.rb and pushed it to this branch: 70992a5. bundle exec rspec spec/parser/flow_sensitive_typing_spec.rb -- 52 examples, 0 failures, run locally.

GitHub Actions hasn't picked up this commit yet (unrelated platform outage), so CI hasn't confirmed this independently yet.

If you were seeing this fail in the downstream project outside this minimal repro, a fuller reproduction (e.g. the local reassigned conditionally, or read across a block boundary) would help narrow down what's different.

apiology added a commit that referenced this pull request Aug 6, 2026
…only inference

Pulls in two new upstream commits on top of the already-merged castwide#1259
base:

- Recognize multi-statement raise/fail branches in flow-sensitive
  typing: always_leaves_compound_statement? now recurses into :begin
  nodes' last child, so a clause like `msg = "bad"; raise msg` is
  still recognized as unconditionally leaving, not just a single bare
  `raise`/`fail` send.
- Infer only the lhs type for `x || raise(...)` and `x ||= raise(...)`:
  new logic in node_chainer.rb and Chain::Or#resolve treats a
  never-returning rhs the same way as the flow-sensitive-typing
  narrowing already does, so the combined type is just the lhs's
  non-nil type instead of a union with the (unreachable) rhs.

Conflict in lib/solargraph/parser/flow_sensitive_typing.rb: the
upstream commits move always_leaves_compound_statement? out of
FlowSensitiveTyping and into the shared
Solargraph::Parser::ParserGem::NodeMethods module (aliased as
Solargraph::Parser::NodeMethods, already included by
FlowSensitiveTyping), so it can be reused from node_chainer.rb, and
extend it with :begin-node support. Removed the now-duplicate local
copy in favor of the shared one; kept this branch's own :closure
attr_reader addition (from #53, needed by every
other FlowSensitiveTyping.new caller already updated on this branch).

Also dropped two now-superfluous `@sg-ignore Need to add nil check
here` comments in node_chainer.rb (above the
always_leaves_compound_statement?(or_asgn_rhs_node) and
always_leaves_compound_statement?(or_rhs_node) calls): on the PR's own
source branch, NodeChainer.chain's node arguments are inferred as
`Array, nil` throughout that branch (a broad, unrelated pre-existing
mismatch visible across dozens of lines when typechecked standalone),
so the @sg-ignore was suppressing a real mismatch there. On this
integration branch those same node variables are already correctly
typed `Parser::AST::Node, nil` (fixed by an earlier-merged PR), and
always_leaves_compound_statement?'s own param is already declared
nilable, so passing them needs no suppression - the local Solargraph
typecheck hook correctly flagged both comments as unneeded.

Verified: spec/parser/flow_sensitive_typing_spec.rb,
spec/parser/node_methods_spec.rb, spec/source/chain/or_spec.rb,
spec/parser/node_chainer_spec.rb (134 examples, 0 failures, 4
pending), and a broader safety net — spec/parser, spec/source,
spec/source_map/clip_spec.rb (475 examples, 0 failures, 14 pending) —
all passing locally.
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