Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion lib/solargraph/api_map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def self.reset_core out: nil
# @param other [Object]
def eql? other
self.class == other.class &&
# @sg-ignore flow sensitive typing needs to handle self.class == other.class
equality_fields == other.equality_fields
end

Expand Down
1 change: 0 additions & 1 deletion lib/solargraph/complex_type/type_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@ def scope
# @param other [Object]
def == other
return false unless self.class == other.class
# @sg-ignore flow sensitive typing should support .class == .class
tag == other.tag
end

Expand Down
6 changes: 0 additions & 6 deletions lib/solargraph/complex_type/unique_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,17 +183,11 @@ def determine_non_literal_name

def eql? other
self.class == other.class &&
# @sg-ignore flow sensitive typing should support .class == .class
@name == other.name &&
# @sg-ignore flow sensitive typing should support .class == .class
@key_types == other.key_types &&
# @sg-ignore flow sensitive typing should support .class == .class
@subtypes == other.subtypes &&
# @sg-ignore flow sensitive typing should support .class == .class
@rooted == other.rooted? &&
# @sg-ignore flow sensitive typing should support .class == .class
@all_params == other.all_params &&
# @sg-ignore flow sensitive typing should support .class == .class
@parameters_type == other.parameters_type
end

Expand Down
1 change: 0 additions & 1 deletion lib/solargraph/equality.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ module Equality
# @return [Boolean]
def eql? other
self.class.eql?(other.class) &&
# @sg-ignore flow sensitive typing should support .class == .class
equality_fields.eql?(other.equality_fields)
end

Expand Down
86 changes: 85 additions & 1 deletion lib/solargraph/parser/flow_sensitive_typing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ class FlowSensitiveTyping
# @param ivars [Array<Solargraph::Pin::InstanceVariable>]
# @param enclosing_breakable_pin [Solargraph::Pin::Breakable, nil]
# @param enclosing_compound_statement_pin [Solargraph::Pin::CompoundStatement, nil]
def initialize locals, ivars, enclosing_breakable_pin, enclosing_compound_statement_pin
# @param closure [Solargraph::Pin::Closure, nil] The pin enclosing
# the code being processed (e.g. the current method), used to
# resolve the declared type of self for narrowing a
# self.class == other.class guard (see #process_class_eq).
def initialize locals, ivars, enclosing_breakable_pin, enclosing_compound_statement_pin, closure = nil
@locals = locals
@ivars = ivars
@enclosing_breakable_pin = enclosing_breakable_pin
@enclosing_compound_statement_pin = enclosing_compound_statement_pin
@closure = closure
end

# @param and_node [Parser::AST::Node]
Expand Down Expand Up @@ -82,6 +87,7 @@ def process_calls node, true_presences, false_presences
process_isa(node, true_presences, false_presences)
process_nilp(node, true_presences, false_presences)
process_bang(node, true_presences, false_presences)
process_class_eq(node, true_presences, false_presences)
end

# @param if_node [Parser::AST::Node]
Expand Down Expand Up @@ -334,6 +340,81 @@ def process_isa isa_node, true_presences, false_presences
process_facts(if_false, false_presences)
end

# @param node [Parser::AST::Node, nil]
# @return [Parser::AST::Node, nil] the receiver being asked for
# its class, e.g. "other" in "other.class", or nil unless
# node is exactly a 0-arg .class call
def parse_class_call node
return if node.nil?
return unless node.type == :send && node.children[1] == :class
return unless (node.children[2..] || []).empty?

node.children[0]
end

# @param node [Parser::AST::Node, nil]
# @return [String, nil] the plain local/instance variable name
# node refers to, or nil unless it is a bare lvar/ivar
# reference
def parse_variable_name node
return if node.nil?
return unless %i[lvar ivar].include?(node.type)

node.children[0].to_s
end

# A guard like "self.class == other.class" (or
# "self.class.eql?(other.class)") guarantees other has the
# same runtime class as self. The runtime class of self is
# always a subtype of the declared instance type of the
# enclosing method, so the runtime class of other is too --
# narrowing other to that declared type is a sound upper
# bound, the same tradeoff #process_isa documents for is_a?.
# Only sound when self is an actual instance: a class methods
# self is the class object itself, whose .class is always
# ::Class rather than the enclosing type, so scope: :class is
# left alone.
#
# @param node [Parser::AST::Node]
# @param true_presences [Array<Range>]
# @param _false_presences [Array<Range>]
#
# @return [void]
def process_class_eq node, true_presences, _false_presences
return unless node.type == :send && %i[== eql?].include?(node.children[1])

enclosing = closure
return unless enclosing && enclosing.scope == :instance

lhs = parse_class_call(node.children[0])
return unless lhs

rhs = parse_class_call(node.children[2])
return unless rhs

other_side = if lhs.type == :self
rhs
elsif rhs.type == :self
lhs
end
return unless other_side

self_type = enclosing.context
return if self_type.undefined?

variable_name = parse_variable_name(other_side)
return unless variable_name

# @sg-ignore Need to add nil check here
position = Range.from_node(node).start

pin = find_var(variable_name, position)
return unless pin

if_true = { pin => [{ type: self_type }] }
process_facts(if_true, true_presences)
end

# @param nilp_node [Parser::AST::Node]
# @return [Array(String, String), nil]
def parse_nilp nilp_node
Expand Down Expand Up @@ -466,6 +547,9 @@ def always_leaves_compound_statement? clause_node
end

attr_reader :locals, :ivars, :enclosing_breakable_pin, :enclosing_compound_statement_pin

# @return [Solargraph::Pin::Closure, nil]
attr_reader :closure
end
end
end
3 changes: 2 additions & 1 deletion lib/solargraph/parser/parser_gem/node_processors/and_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ def process
FlowSensitiveTyping.new(locals,
ivars,
enclosing_breakable_pin,
enclosing_compound_statement_pin).process_and(node)
enclosing_compound_statement_pin,
region.closure).process_and(node)
end
end
end
Expand Down
3 changes: 2 additions & 1 deletion lib/solargraph/parser/parser_gem/node_processors/if_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ def process
FlowSensitiveTyping.new(locals,
ivars,
enclosing_breakable_pin,
enclosing_compound_statement_pin).process_if(node)
enclosing_compound_statement_pin,
region.closure).process_if(node)
condition_node = node.children[0]
if condition_node
pins.push Solargraph::Pin::CompoundStatement.new(
Expand Down
3 changes: 2 additions & 1 deletion lib/solargraph/parser/parser_gem/node_processors/or_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ def process
FlowSensitiveTyping.new(locals,
ivars,
enclosing_breakable_pin,
enclosing_compound_statement_pin).process_or(node)
enclosing_compound_statement_pin,
region.closure).process_or(node)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ def process
FlowSensitiveTyping.new(locals,
ivars,
enclosing_breakable_pin,
enclosing_compound_statement_pin).process_while(node)
enclosing_compound_statement_pin,
region.closure).process_while(node)

# Note - this should not be considered a block, as the
# while statement doesn't create a closure - e.g.,
Expand Down
59 changes: 59 additions & 0 deletions spec/parser/flow_sensitive_typing_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1025,4 +1025,63 @@ def check
clip = api_map.clip_at('test.rb', [13, 12])
expect(clip.infer.to_s).to eq('ReproBase')
end

it 'narrows the other side of a self.class == other.class guard' do
source = Solargraph::Source.load_string(%(
class Repro
# @param other [Object]
def eql?(other)
self.class == other.class &&
name == other.name
end
end
), 'test.rb')
api_map = Solargraph::ApiMap.new.map(source)
clip = api_map.clip_at('test.rb', [5, 20])
expect(clip.infer.to_s).to eq('Repro')
end

it 'narrows the other side of a self.class == other.class guard past a return-unless' do
source = Solargraph::Source.load_string(%(
class Repro
# @param other [Object]
def ==(other)
return false unless self.class == other.class
name == other.name
end
end
), 'test.rb')
api_map = Solargraph::ApiMap.new.map(source)
clip = api_map.clip_at('test.rb', [5, 18])
expect(clip.infer.to_s).to eq('Repro')
end

it 'leaves the declared type in place without a self.class == other.class guard' do
source = Solargraph::Source.load_string(%(
class Repro
# @param other [Object]
def eql?(other)
name == other.name
end
end
), 'test.rb')
api_map = Solargraph::ApiMap.new.map(source)
clip = api_map.clip_at('test.rb', [4, 18])
expect(clip.infer.to_s).to eq('Object')
end

it 'does not narrow other via self.class == other.class in a class method' do
source = Solargraph::Source.load_string(%(
class Repro
# @param other [Object]
def self.same?(other)
self.class == other.class &&
name == other.name
end
end
), 'test.rb')
api_map = Solargraph::ApiMap.new.map(source)
clip = api_map.clip_at('test.rb', [5, 20])
expect(clip.infer.to_s).to eq('Object')
end
end
Loading