Use presence information to improve local variable type probing#1116
Use presence information to improve local variable type probing#1116apiology wants to merge 140 commits intocastwide:masterfrom
Conversation
```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! |
There was a problem hiding this comment.
fix: Ensure Pin::Base#reset_generated! gets called consistently when information is invalidated so that additional rebinding cases work.
|
|
||
| def nullable? | ||
| nil_type? | ||
| end |
There was a problem hiding this comment.
- 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, |
There was a problem hiding this comment.
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.
.github/workflows/plugins.yml
Outdated
| # 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 |
There was a problem hiding this comment.
Use a branch while lekemula/solargraph-rspec#30 is resolved.
| # @sg-ignore | ||
| # @return [undefined] | ||
| def assert_same(other, attr) | ||
| return false if other.nil? |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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>] |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Once we merge variable pins together, we can know about multiple assignments to the same pin
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:
Points to pending external release:
@closureand@locationprivate instance variable meant that we didn't have a chance to refresh our cached derived state. This PR adds a new public API that will be used when available by solargraph-rspec (tested with.responds_to?)Includes for better testing:
Includes for convenience: