Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better hint type handling given to lambda #807

Merged
merged 3 commits into from
May 26, 2023
Merged
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
22 changes: 19 additions & 3 deletions lib/steep/diagnostic/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,19 @@ def header_line
end
end

class ProcHintIgnored < Base
attr_reader :hint_type, :block_node

def initialize(hint_type:, node:)
@hint_type = hint_type
super(node: node)
end

def header_line
"The type hint given to the block is ignored: `#{hint_type}`"
end
end

ALL = ObjectSpace.each_object(Class).with_object([]) do |klass, array|
if klass < Base
array << klass
Expand All @@ -932,7 +945,8 @@ def self.default
UnexpectedTypeArgument => :information,
InsufficientTypeArgument => :information,
UnexpectedTypeArgument => :information,
UnsupportedSyntax => nil
UnsupportedSyntax => nil,
ProcHintIgnored => :information
}
).freeze
end
Expand All @@ -946,7 +960,8 @@ def self.strict
ElseOnExhaustiveCase => nil,
UnknownConstant => nil,
MethodDefinitionMissing => nil,
UnsupportedSyntax => nil
UnsupportedSyntax => nil,
ProcHintIgnored => :warning
}
).freeze
end
Expand All @@ -962,7 +977,8 @@ def self.lenient
MethodDefinitionMissing => nil,
UnexpectedJump => nil,
FalseAssertion => :hint,
UnsupportedSyntax => nil
UnsupportedSyntax => nil,
ProcHintIgnored => :hint
}
).freeze
end
Expand Down
11 changes: 11 additions & 0 deletions lib/steep/subtyping/variable_variance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ def self.add_type(type, variance:, covariants:, contravariants:)
covariants << type.name
contravariants << type.name
end
when AST::Types::Proc
type.type.params.each_type do |type|
add_type(type, variance: variance, covariants: contravariants, contravariants: covariants)
end
add_type(type.type.return_type, variance: variance, covariants: covariants, contravariants: contravariants)
if type.block
type.block.type.params.each_type do |type|
add_type(type, variance: variance, covariants: covariants, contravariants: contravariants)
end
add_type(type.type.return_type, variance: variance, covariants: contravariants, contravariants: covariants)
end
when AST::Types::Union, AST::Types::Intersection, AST::Types::Tuple
type.types.each do |ty|
add_type(ty, variance: variance, covariants: covariants, contravariants: contravariants)
Expand Down
32 changes: 25 additions & 7 deletions lib/steep/type_construction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2904,14 +2904,32 @@ def type_lambda(node, params_node:, body_node:, type_hint:)
block_annotations = source.annotations(block: node, factory: checker.factory, context: nesting)
params = TypeInference::BlockParams.from_node(params_node, annotations: block_annotations)

type_hint = deep_expand_alias(type_hint) if type_hint
if type_hint
original_hint = type_hint

case type_hint
when AST::Types::Proc
params_hint = type_hint.type.params
return_hint = type_hint.type.return_type
block_hint = type_hint.block
self_hint = type_hint.self_type
type_hint = deep_expand_alias(type_hint) || type_hint

procs = flatten_union(type_hint).select do |type|
check_relation(sub_type: type, super_type: AST::Builtin::Proc.instance_type).success?
end

proc_instances, proc_types = procs.partition {|type| AST::Builtin::Proc.instance_type?(type) }

case
when !proc_instances.empty? && proc_types.empty?
# `::Proc` is given as a hint
when proc_types.size == 1
# Proc type is given as a hint
hint_proc = proc_types[0] #: AST::Types::Proc
params_hint = hint_proc.type.params
return_hint = hint_proc.type.return_type
block_hint = hint_proc.block
self_hint = hint_proc.self_type
else
typing.add_error(
Diagnostic::Ruby::ProcHintIgnored.new(hint_type: original_hint, node: node)
)
end
end

block_constr = for_block(
Expand Down
11 changes: 11 additions & 0 deletions sig/steep/diagnostic/ruby.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,17 @@ module Steep
def header_line: () -> String
end

# Type hint is given to a proc/lambda but it was ignored
#
# 1. Because the hint is incompatible to `::Proc` type
# 2. More than one *proc type* is included in the hint
#
class ProcHintIgnored < Base
attr_reader hint_type: AST::Types::t

def initialize: (hint_type: AST::Types::t, node: Parser::AST::Node) -> void
end

# Argument forwarding `...` cannot be done safely, because of
#
# 1. The arguments are incompatible, or
Expand Down
18 changes: 9 additions & 9 deletions sig/steep/subtyping/variable_variance.rbs
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
module Steep
module Subtyping
class VariableVariance
attr_reader covariants: untyped
attr_reader covariants: Set[Symbol]

attr_reader contravariants: untyped
attr_reader contravariants: Set[Symbol]

def initialize: (covariants: untyped, contravariants: untyped) -> void
def initialize: (covariants: Set[Symbol], contravariants: Set[Symbol]) -> void

def covariant?: (untyped var) -> untyped
def covariant?: (Symbol var) -> bool

def contravariant?: (untyped var) -> untyped
def contravariant?: (Symbol var) -> bool

def invariant?: (untyped var) -> untyped
def invariant?: (Symbol var) -> bool

def self.from_type: (AST::Types::t) -> VariableVariance

def self.from_method_type: (untyped method_type) -> untyped
def self.from_method_type: (Interface::MethodType method_type) -> VariableVariance

def self.add_params: (untyped params, block: untyped, covariants: untyped, contravariants: untyped) -> untyped
def self.add_params: (Interface::Function::Params params, block: bool, covariants: Set[Symbol], contravariants: Set[Symbol]) -> void

def self.add_type: (untyped `type`, variance: untyped, covariants: untyped, contravariants: untyped) -> untyped
def self.add_type: (AST::Types::t `type`, variance: :covariant | :contravariant | :invariant, covariants: Set[Symbol], contravariants: Set[Symbol]) -> void
end
end
end
72 changes: 68 additions & 4 deletions test/type_construction_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4677,9 +4677,10 @@ def test_lambda2
with_standard_construction(checker, source) do |construction, typing|
construction.synthesize(source.node)

assert_equal 1, typing.errors.size
typing.errors[0].yield_self do |error|
assert_instance_of Diagnostic::Ruby::IncompatibleAssignment, error
assert_typing_error(typing, size: 2) do |errors|
assert_any!(errors) do |error|
assert_instance_of Diagnostic::Ruby::IncompatibleAssignment, error
end
end
end
end
Expand Down Expand Up @@ -8898,7 +8899,7 @@ def test_lambda_with_block_non_proc
with_standard_construction(checker, source) do |construction, typing|
type, _, _ = construction.synthesize(source.node)

assert_typing_error(typing, size: 1) do |errors|
assert_typing_error(typing, size: 2) do |errors|
assert_any!(errors) do |error|
assert_instance_of Diagnostic::Ruby::IncompatibleAssignment, error
end
Expand Down Expand Up @@ -10747,4 +10748,67 @@ class ReturnsProc
end
end
end

def test_type_hint__proc_optional
with_checker(<<~RBS) do |checker|
RBS
source = parse_ruby(<<~RUBY)
# @type var proc: nil | ^(Integer) -> String
proc = -> (x) do
x.hogehoge
end
RUBY

with_standard_construction(checker, source) do |construction, typing|
type, _, context = construction.synthesize(source.node)

assert_typing_error(typing, size: 1) do |errors|
assert_any!(errors) do |error|
assert_instance_of Diagnostic::Ruby::NoMethod, error
assert_equal parse_type("::Integer"), error.type
end
end
end
end
end

def test_type_hint__proc_union_with_proc
with_checker(<<~RBS) do |checker|
RBS
source = parse_ruby(<<~RUBY)
# @type var proc: (^(Integer) -> String) | (^(String, String) -> Integer)
proc = -> (x) do
x.hogehoge
end
RUBY

with_standard_construction(checker, source) do |construction, typing|
type, _, context = construction.synthesize(source.node)

assert_typing_error(typing, size: 1) do |errors|
assert_any!(errors) do |error|
assert_instance_of Diagnostic::Ruby::ProcHintIgnored, error
assert_equal parse_type("(^(::Integer) -> ::String) | (^(::String, ::String) -> ::Integer)"), error.hint_type
end
end
end
end
end

def test_type_hint__proc_union_proc_instance
with_checker(<<~RBS) do |checker|
RBS
source = parse_ruby(<<~RUBY)
# @type var proc: Proc
proc = -> (x) do
x.hogehoge
end
RUBY

with_standard_construction(checker, source) do |construction, typing|
type, _, context = construction.synthesize(source.node)
assert_no_error typing
end
end
end
end