Skip to content

Use presence information to improve local variable type probing#1116

Open
apiology wants to merge 140 commits intocastwide:masterfrom
apiology:local_variable_probiing
Open

Use presence information to improve local variable type probing#1116
apiology wants to merge 140 commits intocastwide:masterfrom
apiology:local_variable_probiing

Conversation

@apiology
Copy link
Contributor

@apiology apiology commented Oct 22, 2025

Use Pin::LocalVariable#combine_with to merge information from multiple assignments to the same local variable that may be visible from a given location, offering a single pin with consolidated information with ApiMap#var_at_location.

This, combined with #1114 and #1119, enables even more flow-sensitive typing scenarios, making nil-checking more accurate. Examples from a dev branch of enabled scenarios:

Fixes needed to make that happen:

  • fix: Handle a number of Pin::Block rebinding cases more consistently so that we can rely on the closure information inside to determine visibility of assignments.
  • fix: Ensure Pin::Base#reset_generated! gets called consistently when information is invalidated so that rebinding cases work more consistently.
  • fix: Ensure #reduce_class_type is in both ComplexType and UniqueType, a bug this PR tickled
  • fix: Track location information more consistently so that we can use it to determine visibility of assignments.

Points to pending external release:

Includes for better testing:

Includes for convenience:

apiology and others added 30 commits July 13, 2025 12:44
```sh
$ SOLARGRAPH_ASSERTS=on bundle exec solargraph method_pin --rbs 'RuboCop::AST::ArrayNode#values'
def values: () -> Array
$ bundle exec solargraph help method_pin
Usage:
  solargraph method_pin [PATH]

Options:
  [--rbs], [--no-rbs], [--skip-rbs]           # Output the pin as RBS
                                              # Default: false
  [--typify], [--no-typify], [--skip-typify]  # Output the calculated return type of the pin from annotations
                                              # Default: false
  [--probe], [--no-probe], [--skip-probe]     # Output the calculated return type of the pin from annotations and inference
                                              # Default: false
  [--stack], [--no-stack], [--skip-stack]     # Show entire stack by including definitions in superclasses
                                              # Default: false

Describe a method pin
$
```
* Allow newer RBS gem versions

This allow users to upgrade to recent Tapioca versions.

Tapioca now requires newish versions of the spoom gem, which depends on 4.x
pre-releases of the rbs gem.

* Add RBS version to test matrix

* Add exclude rule

* Move to 3.6.1
* Look for external requires before cataloging bench

It seems like sync_catalog will go through the motions but not
actually load pins from gems here due to passing an empty requires
array to ApiMap.

I'm sure those requires get pulled in eventually, but we go through at
least one catalog cycle without it happening.

Found while trying to test a different issue but not being able to get
completions from a gem in a spec.

* Ensure backport is pre-cached
…astwide#1011)

* Complain in strong type-checking if an @sg-ignore line is not needed

* Fix return type

* Fix spec

* Linting/coverage fixes

* Coverage fix
* Document a log level env variable

* Fix logger reference

* Fix env var name

* Populate location information from RBS files (castwide#768)

* Populate location information from RBS files

The 'rbs' gem maps the location of different definitions to the
relevant point in the RGS files themselves - this change provides the
ability to jump into the right place in those files to see the type
definition via the LSP.

* Prefer source location in language server

* Resolve merge issue

* Fix Path vs String type error

* Consolidate parameter handling into Pin::Callable (castwide#844)

* Consolidate parameter handling into Pin::Closure

* Clarify clobbered variable names

* Fix bug in to_rbs, add spec, then fix new bug found after running spec

* Catch one more Signature.new to translate from strict typechecking

* Introduce Pin::Callable

* Introduce Pin::Callable

* Introduce Pin::Callable

* Introduce Pin::Callable

* Introduce Pin::Callable

* Introduce Pin::Callable

* Introduce Pin::Callable

* Use Pin::Callable type in args_node.rb

* Select String#each_line overload with mandatory vs optional arg info

* Adjust local variable presence to start after assignment, not before (castwide#864)

* Adjust local variable presence to start after assignment, not before

* Add regression test around assignment in return position

* Fix assignment visibility code, which relied on bad asgn semantics

* Resolve params from ref tags (castwide#872)

* Resolve params from ref tags

* Resolve ref tags with namespaces

* Fix merge issue

* RuboCop fixes

* Add @sg-ignore

* Linting

* Linting fix

* Linting fix

---------

Co-authored-by: Fred Snyder <fsnyder@castwide.com>
* Fix hole in type checking evaluation

The call to `bar()` in `[ bar('string') ].compact` is not currently
type-checked due to a missed spot in `NodeMethods#call_nodes_from(node)`

* Avoid over-reporting call issues

* Fix annotation

* Add nocov markers around unreachable area

* Fix a coverage issue

* Cover more paths in type checking

* Fix a code coverage issue

* Drop blank line

* Ratchet Rubocop todo

* Fix missing coverage
If we know the target of an unresolved method call, include it in the
error message.
* Internal strict type-checking fixes

* Add annotation

* Add annotation

* Add @sg-ignores for now
* Reproduce and fix a ||= (or-asgn) evaluation issue

* Fix linting issue
This isn't used anywhere to my knowledge, but it makes sense to think
of symbols as being in the global namespace, helps guarantee that
closure is always available on a pin, and of course keeps the assert
happy ;)
* Fix 'all!' config to reporters

Solargraph found the type error here!

* Linting fixes
* Fix RuboCop linting errors in regular expressions

* Continue on rubocop_todo errors

* Move configuration

* Continue on undercover errors
* Resolve class aliases via Constant pins

This also eliminates the need for Parser::NodeMethods as a searately
defined class.

* Resolve merge issues

* Resolve Solargraph strong complaint

* Add @sg-ignore

* Fix RuboCop issues

* Drop unused method

* Ratchet .rubocop_todo.yml
* Improve performance of resolve_method_aliases method

- Add indexed lookups for methods and aliases by name
- Cache ancestor traversal to avoid repeated computations
- Separate regular pins from aliases for more efficient processing
- Replace linear search with direct indexed method lookup
- Add fast path for creating resolved alias pins without individual lookups

Generated with Claude Code

* Update .rubocop_todo.yml

* Fix typechecking - get_method_stack order

`get_method_stack` returns the following order for `Enumerable#select`:

- master branch:

=> ["Enumerable#select", "Kernel#select"]

- current branch:

=> ["Kernel#select", "Enumerable#select"]

* Avoid redundant indexing methods_by_name

loop through ancestors and rely on store.get_path_pins

new_pin.docstring.add_tag(tag)
redefine_return_type new_pin, tag
new_pin.reset_generated!
Copy link
Contributor Author

Choose a reason for hiding this comment

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

fix: Ensure Pin::Base#reset_generated! gets called consistently when information is invalidated so that additional rebinding cases work.


def nullable?
nil_type?
end
Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • fix: Ensure #reduce_class_type and #nullable? are in both ComplexType and UniqueType, a bug this PR tickled

receiver: node.children[0],
comments: comments_for(node),
scope: region.scope || region.closure.context.scope,
scope: scope,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

fix: Handle a number of Pin::Block rebinding cases more consistently so that we can rely on the closure information inside to determine visibility of assignments.

Specifically, handle a .class_eval case by directly setting the binder rather than pretending this is in a different closure. A .class_eval block is its own closure, with its parent closure being the call site, not the class. The class being evaled within is still the binder for purposes of method calls.

# pending https://github.com/lekemula/solargraph-rspec/pull/30
git clone https://github.com/apiology/solargraph-rspec.git
cd solargraph-rspec
git checkout reset_closures
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Use a branch while lekemula/solargraph-rspec#30 is resolved.

# @sg-ignore
# @return [undefined]
def assert_same(other, attr)
return false if other.nil?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was always a bug - complained about by strong typechecking

# @param context [ComplexType, nil]
def initialize visibility: :public, explicit: true, block: :undefined, node: nil, attribute: false, signatures: nil, anon_splat: false,
**splat
context: nil, **splat
Copy link
Contributor Author

@apiology apiology Nov 14, 2025

Choose a reason for hiding this comment

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

fix: This allows def inside class_evals to work correctly - in that case, our closure is not the same as our context

@return_type = nil
@full_context = nil
@path = nil
end
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ensure that closure resets also reset the context and path

end
return inferred_pins(found, api_map, name_pin, locals) unless found.empty?
found = api_map.var_at_location(locals, word, name_pin, location) if head?
return inferred_pins([found], api_map, name_pin, locals) unless found.nil?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only need to consider a single variable pin - the hard work has been done by combine_with()

private

# @param pins [::Enumerable<Pin::Method>]
# @param pins [::Enumerable<Pin::Base>]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was never right

class InstanceVariable < Link
def resolve api_map, name_pin, locals
api_map.get_instance_variable_pins(name_pin.binder.namespace, name_pin.binder.scope).select{|p| p.name == word}
api_map.get_instance_variable_pins(name_pin.context.namespace, name_pin.context.scope).select{|p| p.name == word}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Again, instance variables come from the context, not the binder

# @return [Parser::AST::Node, nil]
attr_reader :assignment
# @return [Array<Parser::AST::Node>]
attr_reader :assignments
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Once we merge variable pins together, we can know about multiple assignments to the same pin

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.

3 participants