Skip to content
Closed
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
8 changes: 8 additions & 0 deletions lib/solargraph/api_map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,14 @@ def type_include? host_ns, module_ns
store.get_includes(host_ns).map { |inc_tag| inc_tag.type.name }.include?(module_ns)
end

# Check whether a namespace is a module rather than a class.
#
# @param fqns [String] A fully qualified namespace
# @return [Boolean]
def module? fqns
get_namespace_type(fqns) == :module
end

# @param pins [Enumerable<Pin::Base>]
# @param visibility [Enumerable<Symbol>]
# @return [Array<Pin::Base>]
Expand Down
10 changes: 10 additions & 0 deletions lib/solargraph/complex_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -401,11 +401,21 @@ def intersect_with intersection_type, api_map
# try to find common types via conformance
items.each do |ut|
intersection_type.each do |int_type|
# Two branches share a body without sharing a meaning, and merging
# them would reorder the tests and change which type survives.
# rubocop:disable Lint/DuplicateBranch
if int_type.conforms_to?(api_map, ut, :assignment)
types << int_type
elsif ut.conforms_to?(api_map, int_type, :assignment)
types << ut
elsif api_map.module?(int_type.name) || api_map.module?(ut.name)
# Two classes are disjoint under single inheritance, so a member
# failing the guard is dropped. A module on either side is not:
# a subclass can mix it in. Keeping the guard is the closest sound
# answer available without an intersection type.
types << int_type
end
# rubocop:enable Lint/DuplicateBranch
end
end
types = [ComplexType::UniqueType::UNDEFINED] if types.empty?
Expand Down
12 changes: 12 additions & 0 deletions lib/solargraph/complex_type/unique_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,23 @@ def intersect_with intersection_type, api_map
# try to find common types via conformance
items.each do |ut|
intersection_type.each do |int_type|
# Two branches share a body without sharing a meaning. Merging them
# here would be safe, but ComplexType#intersect_with tests the
# conformance directions in the opposite order, where it would not
# — so both copies keep the same shape.
# rubocop:disable Lint/DuplicateBranch
if ut.conforms_to?(api_map, int_type, :assignment)
types << ut
elsif int_type.conforms_to?(api_map, ut, :assignment)
types << int_type
elsif api_map.module?(int_type.name) || api_map.module?(ut.name)
# Two classes are disjoint under single inheritance, so a member
# failing the guard is dropped. A module on either side is not:
# a subclass can mix it in. Keeping the guard is the closest sound
# answer available without an intersection type.
types << int_type
end
# rubocop:enable Lint/DuplicateBranch
end
end
types = [ComplexType::UniqueType::UNDEFINED] if types.empty?
Expand Down
11 changes: 11 additions & 0 deletions spec/api_map_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,17 @@ class Bar < Bar; end
expect(@api_map.super_and_sub?('Foo', 'Bar')).to be(false)
end

it 'distinguishes modules from classes and unknown namespaces' do
source = Solargraph::Source.load_string(%(
module Foo; end
class Bar; end
))
@api_map.map source
expect(@api_map.module?('Foo')).to be(true)
expect(@api_map.module?('Bar')).to be(false)
expect(@api_map.module?('Baz')).to be(false)
end

it 'adds prepended methods to the ancestor tree' do
source = Solargraph::Source.load_string(%(
module Prepended
Expand Down
33 changes: 33 additions & 0 deletions spec/complex_type/unique_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,37 @@
expect(types_encountered).to eq([type])
end
end

# ComplexType#intersect_with is a sibling copy of this one, tested in
# complex_type_spec.rb. Both are covered so the two cannot drift apart.
describe '#intersect_with' do
let(:api_map) do
api_map = Solargraph::ApiMap.new
api_map.map Solargraph::Source.load_string(%(
module M; end
class A; end
class A_with_M < A; include M; end
), 'test.rb')
api_map
end

let(:guard) { Solargraph::ComplexType.parse('M').qualify(api_map, '') }

it 'keeps the guard when neither type implies the other and one is a module' do
declared = described_class.parse('A').qualify(api_map, '')
expect(declared.intersect_with(guard, api_map).rooted_tags).to eq('::M')
end

it 'keeps the narrower type when the declared type already implies the guard' do
declared = described_class.parse('A_with_M').qualify(api_map, '')
expect(declared.intersect_with(guard, api_map).rooted_tags).to eq('::A_with_M')
end

it 'still admits a value satisfying both the declared type and the guard' do
declared = described_class.parse('A').qualify(api_map, '')
inhabitant = Solargraph::ComplexType.parse('A_with_M').qualify(api_map, '')
narrowed = declared.intersect_with(guard, api_map)
expect(inhabitant.conforms_to?(api_map, narrowed, :assignment)).to be(true)
end
end
end
60 changes: 60 additions & 0 deletions spec/complex_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -765,4 +765,64 @@ def make_bar
expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true)
end
end

context 'when narrowing a union down to the members that pass a mixin guard' do
let(:mixin_api_map) do
api_map = Solargraph::ApiMap.new
api_map.map Solargraph::Source.load_string(%(
module M; end
class A; end
class B; end
class A_with_M < A; include M; end
class B_with_M < B; include M; end
), 'test.rb')
api_map
end

# A_with_M satisfies the declared A and mixes in M, so it is a real
# inhabitant of 'A, B_with_M' that an is_a?(M) guard admits.
let(:declared) { Solargraph::ComplexType.parse('A, B_with_M').qualify(mixin_api_map, '') }
let(:guard) { Solargraph::ComplexType.parse('M').qualify(mixin_api_map, '') }
let(:inhabitant) { Solargraph::ComplexType.parse('A_with_M').qualify(mixin_api_map, '') }

it 'still admits a value satisfying both the declared type and the guard' do
narrowed = declared.intersect_with(guard, mixin_api_map)
expect(inhabitant.conforms_to?(mixin_api_map, narrowed, :assignment)).to be(true)
end

it 'intersects the member the guard neither implies nor is implied by' do
pending 'https://github.com/castwide/solargraph/pull/1231'
narrowed = declared.intersect_with(guard, mixin_api_map)
expect(narrowed.rooted_tags).to eq('::A & ::M, ::B_with_M')
end
end

context 'when narrowing a union whose module member could satisfy a class guard' do
let(:mirror_api_map) do
api_map = Solargraph::ApiMap.new
api_map.map Solargraph::Source.load_string(%(
module M1; end
class C; end
class B; end
class D < C; include M1; end
), 'test.rb')
api_map
end

# D subclasses C and mixes in M1, so it is a real inhabitant of
# 'M1, B' that an is_a?(C) guard admits.
let(:declared) { Solargraph::ComplexType.parse('M1, B').qualify(mirror_api_map, '') }
let(:guard) { Solargraph::ComplexType.parse('C').qualify(mirror_api_map, '') }
let(:inhabitant) { Solargraph::ComplexType.parse('D').qualify(mirror_api_map, '') }

it 'still admits a value satisfying both the declared type and the guard' do
narrowed = declared.intersect_with(guard, mirror_api_map)
expect(inhabitant.conforms_to?(mirror_api_map, narrowed, :assignment)).to be(true)
end

it 'drops the class member that single inheritance proves disjoint' do
narrowed = declared.intersect_with(guard, mirror_api_map)
expect(narrowed.rooted_tags).to eq('::C')
end
end
end
Loading