Skip to content

Name where inference stopped in typecheck inference errors - #1313

Draft
apiology wants to merge 3 commits into
castwide:masterfrom
apiology:link-blame-typecheck
Draft

Name where inference stopped in typecheck inference errors#1313
apiology wants to merge 3 commits into
castwide:masterfrom
apiology:link-blame-typecheck

Conversation

@apiology

Copy link
Copy Markdown
Contributor

Problem

A local with a declared type, whose expression calls a method that does not exist:

class Report
  # @return [void]
  def run
    # @type [String]
    v = 'a'.no_such.upcase
    v
    nil
  end
end

typecheck --level strong reports one problem for this file, and it names only the variable:

example1.rb:5: Variable type could not be inferred for v

Nothing says which of 'a', no_such, or upcase inference stopped at, and there is no other problem reported in the file to cross-reference. Diagnosing it means bisecting the expression by hand.

The same gap appears in code with no tags at all, where the method-return check fires instead:

class Report
  # @return [Array<String>]
  def rows
    ys = fetch.to_a.compact
    ys
  end

  # @return [Rows::Unknown]
  def fetch
    fetch
  end
end
example2.rb:3: Report#rows return type could not be inferred
example2.rb:9: Unresolved return type Rows::Unknown for Report#fetch

The first message names the method, not the part of the expression responsible. The second names the underlying cause but sits at def fetch on line 9, not at the expression in rows that failed — and in a real file it can be far from it, among unrelated problems.

Solution

Both messages now carry a plain-language cause naming where inference stopped and what it was called on:

example1.rb:5: Variable type could not be inferred for v: `no_such` could not be resolved on String
example2.rb:3: Report#rows return type could not be inferred: could not determine the return type of `fetch` on Report

A name that matched no definition renders as `name` could not be resolved on Type; a definition whose declared return type will not resolve renders as could not determine the return type of `name` on Type. Messages stay byte-identical when no cause can be named, and when the failing call is the one already named.

The method-return path matters most for code with no @type tags at all, which is the common case. In the second example the return value is the bare variable ys, so the walk continues into that variable's own assignment — which is why the cause names fetch, a call that appears nowhere in the return expression itself.

Mechanism: Chain#trace re-walks the chain exactly as #define does, recording each link's receiver type, matched pin count, and inferred type; Chain#first_undefined_link returns the first failing record. Both are uncached and only invoked on the error path. Pin::BaseVariable#probe_blame and Pin::Method#probe_blame apply the walk to assignments and return nodes respectively.

Test plan: a message matrix in strong_spec asserts the complete rendered string for ten cases — four expression shapes, each in an untagged and a tagged form, plus a control asserting that correcting a misspelled method name reports nothing at all, plus a guardrail asserting the message is unchanged when only the final call fails. Six of the ten fail before the change; the other four pin behavior that must not shift. Three existing specs asserting the exact old method-return string were updated to the superset text. Two pending specs record problems found while testing: an unrelated parser gap, and a missing diagnostic for a call to a nonexistent method on an untagged local. That second case is pinned from both sides: an active spec asserts that no variable-validation message is produced today, and the pending spec asserts the diagnostic that should be reported. Full suite: 1638 examples, 5 failures, all 5 in spec/diagnostics and all 5 reproducing identically on unmodified master.

Opened as a draft. This PR was written by Claude (Anthropic's Claude Code) on behalf of @apiology.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT

'Variable type could not be inferred for X' and 'P#m return type
could not be inferred' name the assigned variable or the method, not the
part of the expression where inference stopped. Both now carry a
plain-language cause naming it:

  Variable type could not be inferred for n: `lenght` could not be
  resolved on Array<String>

  Report#run return type could not be inferred: `lenght` could not be
  resolved on Array<String>

A name that matched no definition renders as '`name` could not be
resolved on Type'; a definition whose return type will not resolve
renders as 'could not determine the return type of `name` on Type'.
Messages are byte-identical to before whenever no cause can be named, or
when the failing call is the one already named.

The method-return path matters most for code with no @type tags at all,
which is the common case: a bare-variable return delegates to that
variable's own assignment, so the cause survives one hop.

Substrate for future diagnostics: Chain#trace records per-link
resolution (receiver type, pin count, inferred type) and
Chain#first_undefined_link exposes the first failure; both uncached,
error-path only. Pin::BaseVariable#probe_blame and
Pin::Method#probe_blame apply the walk to assignments and return nodes.

The spec matrix pairs each shape: the no-@type form first, then the
tagged variant, since most code carries no tags. Three existing specs
asserting the exact old method-return string were updated to the
superset text (strong_spec x2, alpha_spec x1). Two pending specs record
problems found while testing: an unrelated parser gap, and a plain typo
on an untagged local that produces no diagnostic at all.

Suite: 1638 examples, 0 failures attributable (5 diagnostics failures
are a pre-existing spec-ordering artifact, reproduced identically on
stock master with the same chunk pairing).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT
Comment thread lib/solargraph/pin/base_variable.rb Outdated
next if node.nil? || node.type == :NIL || node.type == :nil
rng = Range.from_node(node)
next if rng.nil?
# @sg-ignore Need to add nil check here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the actual typecheck error here? If it's related to the above line's nil check, is there an open PR or issue addressing that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual error, from solargraph typecheck --level strong lib/solargraph/pin/base_variable.rb with only this line removed:

lib/solargraph/pin/base_variable.rb:190: Unresolved call to filename on Solargraph::Location, nil

That is not a tool limitation ��� the nil check really was missing. Pin::Base#location is typed Location, nil, and probe_blame here guarded only closure.nil? before calling location.filename. Pin::Method#probe_blame, added in this same PR, does guard it: return nil if node.nil? || method_body_node.nil? || location.nil?.

The guard is now extended to return nil if closure.nil? || location.nil?.

Measured after adding the guard: removing the @sg-ignore again still reports the identical Unresolved call to filename on Solargraph::Location, nil at that line, so the suppression is still needed and its cause is now the same return-guard narrowing gap as the line below ��� #1254. Retagged to match, in the same wording used in Pin::Method#probe_blame.

This comment was written by Claude Code on behalf of @apiology.

clip = api_map.clip_at(location.filename, rng.ending)
chain = Parser.chain(node, nil, nil)
# @sg-ignore closure nil-guarded at method entry; return-guard
# narrowing not tracked - https://github.com/castwide/solargraph/issues/1254

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto on actual error

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lib/solargraph/pin/base_variable.rb:193: Wrong argument type for Solargraph::Source::Chain#first_undefined_link: name_pin expected Solargraph::Pin::Base, received Solargraph::Pin::Closure, nil

closure is Pin::Closure, nil and is guarded by return nil if closure.nil? at method entry, but the narrowing is not carried past the return guard ��� #1254 ("Flow-sensitive typing doesn't narrow past a raise/return-based nil guard"). No code change here; the existing tag matches the observed error.

This comment was written by Claude Code on behalf of @apiology.

apiology and others added 2 commits August 18, 2026 11:58
Pin::Base#location is typed Location, nil. Pin::BaseVariable#probe_blame
guarded only closure.nil? and then called location.filename, so a pin
without a location raised NoMethodError on the diagnostic path.
Pin::Method#probe_blame already guards location.nil? at method entry;
this makes the variable side match.

Removing the @sg-ignore on that line reported:

  lib/solargraph/pin/base_variable.rb:190: Unresolved call to filename
  on Solargraph::Location, nil

which was a real missing check rather than a typechecker limitation, so
the comment text no longer described what it was suppressing. With the
guard in place, removing the @sg-ignore still reports the same error,
because flow-sensitive typing does not carry the narrowing past a
return-based nil guard - castwide#1254. Retagged with that
cause, in the same wording used in Pin::Method#probe_blame.

Verified against the pre-change branch: rspec 1638 examples / 0 failures
/ 62 pending, rubocop 71 offenses, solargraph typecheck --level strong
527 problems - all three unchanged.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CXmnT5gSB1PheL9UbiGEVA
The comment shipped a literal "see #NNNN". No open castwide/solargraph
issue matches numblock or numbered-parameter handling, so there is no
number to fill in. The sentence stands on its own without the pointer.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CXmnT5gSB1PheL9UbiGEVA
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