From bf0d16981f25ad4ea39be8b5f7e7c3c5f9e08d53 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Fri, 30 Aug 2024 14:27:21 +0900 Subject: [PATCH 01/11] Fix test --- test/type_check_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/type_check_test.rb b/test/type_check_test.rb index bf10f0a96..4cb7362ef 100644 --- a/test/type_check_test.rb +++ b/test/type_check_test.rb @@ -952,7 +952,7 @@ def test_and_shortcut__truthy signatures: {}, code: { "a.rb" => <<~RUBY - x = [1].first + x = [1].find { true } 1 and return unless x x + 1 RUBY @@ -970,7 +970,7 @@ def test_and_shortcut__false signatures: {}, code: { "a.rb" => <<~RUBY - x = [1].first + x = [1].find { true } return and true unless x x + 1 RUBY @@ -988,7 +988,7 @@ def test_or_shortcut__nil signatures: {}, code: { "a.rb" => <<~RUBY - x = [1].first + x = [1].find { true } nil or return unless x x + 1 RUBY @@ -1006,7 +1006,7 @@ def test_or_shortcut__false signatures: {}, code: { "a.rb" => <<~RUBY - x = [1].first + x = [1].find { true } x or return unless x x + 1 RUBY From 230da2e972323740714454c42e84f54630e86b98 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Fri, 30 Aug 2024 14:43:42 +0900 Subject: [PATCH 02/11] Bundle rbs-3.6.0.dev --- Gemfile | 2 +- Gemfile.lock | 5 ++--- steep.gemspec | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index cd6478cdb..5530c0f0d 100644 --- a/Gemfile +++ b/Gemfile @@ -16,4 +16,4 @@ group :development, optional: true do gem "majo" end -gem "rbs" +# gem "rbs", path: "../rbs" diff --git a/Gemfile.lock b/Gemfile.lock index ea31d4db1..74f3452b0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,7 +12,7 @@ PATH logger (>= 1.3.0) parser (>= 3.1) rainbow (>= 2.2.2, < 4.0) - rbs (>= 3.5.0.pre) + rbs (~> 3.6.0.dev) securerandom (>= 0.1) strscan (>= 1.0.0) terminal-table (>= 2, < 4) @@ -73,7 +73,7 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rbs (3.5.2) + rbs (3.6.0.dev.1) logger rdoc (6.7.0) psych (>= 4.0.0) @@ -101,7 +101,6 @@ DEPENDENCIES minitest-hooks minitest-slow_test rake - rbs stackprof steep! vernier (~> 1.0) diff --git a/steep.gemspec b/steep.gemspec index bda1a46f3..934e9eef3 100644 --- a/steep.gemspec +++ b/steep.gemspec @@ -36,7 +36,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency "rainbow", ">= 2.2.2", "< 4.0" spec.add_runtime_dependency "listen", "~> 3.0" spec.add_runtime_dependency "language_server-protocol", ">= 3.15", "< 4.0" - spec.add_runtime_dependency "rbs", ">= 3.5.0.pre" + spec.add_runtime_dependency "rbs", "~> 3.6.0.dev" spec.add_runtime_dependency "concurrent-ruby", ">= 1.1.10" spec.add_runtime_dependency "terminal-table", ">= 2", "< 4" spec.add_runtime_dependency "securerandom", ">= 0.1" From cc22a4171c36bf955cc53c3c924dbace03c62ce8 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Fri, 30 Aug 2024 14:43:48 +0900 Subject: [PATCH 03/11] Type check logger --- lib/steep.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/steep.rb b/lib/steep.rb index ee985aac8..81cedc734 100644 --- a/lib/steep.rb +++ b/lib/steep.rb @@ -164,6 +164,11 @@ def self.ui_logger def self.new_logger(output, prev_level) logger = Logger.new(output) logger.formatter = proc do |severity, datetime, progname, msg| + # @type var severity: String + # @type var datetime: Time + # @type var progname: untyped + # @type var msg: untyped + # @type block: String "#{datetime.strftime('%Y-%m-%d %H:%M:%S.%L')}: #{severity}: #{msg}\n" end ActiveSupport::TaggedLogging.new(logger).tap do |logger| From 40987a38b424adc68d074a9d6690ff8784f64910 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Fri, 30 Aug 2024 14:43:57 +0900 Subject: [PATCH 04/11] Support any upper bound --- lib/steep/ast/types/factory.rb | 2 +- lib/steep/server/lsp_formatter.rb | 4 +- lib/steep/signature/validator.rb | 12 ++--- lib/steep/type_construction.rb | 14 +++--- .../type_inference/logic_type_interpreter.rb | 4 ++ test/type_check_test.rb | 45 +++++++++++++++++++ 6 files changed, 65 insertions(+), 16 deletions(-) diff --git a/lib/steep/ast/types/factory.rb b/lib/steep/ast/types/factory.rb index 263328086..9b133c02a 100644 --- a/lib/steep/ast/types/factory.rb +++ b/lib/steep/ast/types/factory.rb @@ -245,7 +245,7 @@ def params(type) def type_param(type_param) Interface::TypeParam.new( name: type_param.name, - upper_bound: type_opt(type_param.upper_bound), + upper_bound: type_opt(type_param.upper_bound_type), variance: type_param.variance, unchecked: type_param.unchecked? ) diff --git a/lib/steep/server/lsp_formatter.rb b/lib/steep/server/lsp_formatter.rb index e1a68be70..15b4ce8b0 100644 --- a/lib/steep/server/lsp_formatter.rb +++ b/lib/steep/server/lsp_formatter.rb @@ -375,8 +375,8 @@ def name_and_params(name, params) end s << param.name.to_s - if param.upper_bound - s << " < #{param.upper_bound.to_s}" + if param.upper_bound_type + s << " < #{param.upper_bound_type.to_s}" end s diff --git a/lib/steep/signature/validator.rb b/lib/steep/signature/validator.rb index 07df36765..a0a8c19fd 100644 --- a/lib/steep/signature/validator.rb +++ b/lib/steep/signature/validator.rb @@ -79,8 +79,8 @@ def validate_type_application_constraints(type_name, type_params, type_args, loc type_params.zip(type_args).each do |param, arg| arg or raise - if param.upper_bound - upper_bound_type = factory.type(param.upper_bound).subst(subst) + if param.upper_bound_type + upper_bound_type = factory.type(param.upper_bound_type).subst(subst) arg_type = factory.type(arg) constraints = Subtyping::Constraints.empty @@ -236,7 +236,7 @@ def each_variable_type(definition) def validate_definition_type(definition) each_method_type(definition) do |method_type| upper_bounds = method_type.type_params.each.with_object({}) do |param, hash| - hash[param.name] = factory.type_opt(param.upper_bound) + hash[param.name] = factory.type_opt(param.upper_bound_type) end checker.push_variable_bounds(upper_bounds) do @@ -264,7 +264,7 @@ def validate_one_class_decl(name, entry) Steep.logger.tagged "#{name}" do builder.build_instance(name).tap do |definition| upper_bounds = definition.type_params_decl.each.with_object({}) do |param, bounds| - bounds[param.name] = factory.type_opt(param.upper_bound) + bounds[param.name] = factory.type_opt(param.upper_bound_type) end self_type = AST::Types::Name::Instance.new( @@ -480,7 +480,7 @@ def validate_one_interface(name) definition = builder.build_interface(name) upper_bounds = definition.type_params_decl.each.with_object({}) do |param, bounds| - bounds[param.name] = factory.type_opt(param.upper_bound) + bounds[param.name] = factory.type_opt(param.upper_bound_type) end self_type = AST::Types::Name::Interface.new( @@ -575,7 +575,7 @@ def validate_one_alias(name, entry = env.type_alias_decls[name]) end upper_bounds = entry.decl.type_params.each.with_object({}) do |param, bounds| - bounds[param.name] = factory.type_opt(param.upper_bound) + bounds[param.name] = factory.type_opt(param.upper_bound_type) end validator.validate_type_alias(entry: entry) do |type| diff --git a/lib/steep/type_construction.rb b/lib/steep/type_construction.rb index eafb4f474..231d3100e 100644 --- a/lib/steep/type_construction.rb +++ b/lib/steep/type_construction.rb @@ -195,11 +195,11 @@ def for_new_method(method_name, node, args:, self_type:, definition:) end method_params = - if method_type - TypeInference::MethodParams.build(node: node, method_type: method_type) - else - TypeInference::MethodParams.empty(node: node) - end + if method_type + TypeInference::MethodParams.build(node: node, method_type: method_type) + else + TypeInference::MethodParams.empty(node: node) + end method_context = TypeInference::Context::MethodContext.new( name: method_name, @@ -405,7 +405,7 @@ def for_module(node, new_module_name) type_params = definition.type_params_decl.map do |param| Interface::TypeParam.new( name: param.name, - upper_bound: checker.factory.type_opt(param.upper_bound), + upper_bound: checker.factory.type_opt(param.upper_bound_type), variance: param.variance, unchecked: param.unchecked? ) @@ -494,7 +494,7 @@ def for_class(node, new_class_name, super_class_name) type_params = definition.type_params_decl.map do |type_param| Interface::TypeParam.new( name: type_param.name, - upper_bound: type_param.upper_bound&.yield_self {|t| checker.factory.type(t) }, + upper_bound: type_param.upper_bound_type&.yield_self {|t| checker.factory.type(t) }, variance: type_param.variance, unchecked: type_param.unchecked?, location: type_param.location diff --git a/lib/steep/type_inference/logic_type_interpreter.rb b/lib/steep/type_inference/logic_type_interpreter.rb index 5f0a45665..052b4eec0 100644 --- a/lib/steep/type_inference/logic_type_interpreter.rb +++ b/lib/steep/type_inference/logic_type_interpreter.rb @@ -77,6 +77,10 @@ def evaluate_node(env:, node:, type: typing.type_of(node: node)) ] end + if type.is_a?(AST::Types::Var) + type = config.upper_bound(type.name) || type + end + case node.type when :lvar name = node.children[0] diff --git a/test/type_check_test.rb b/test/type_check_test.rb index 4cb7362ef..dbceb60ca 100644 --- a/test/type_check_test.rb +++ b/test/type_check_test.rb @@ -2141,4 +2141,49 @@ def self.new: (Integer size) -> instance YAML ) end + + def test_any_upperbound + run_type_check_test( + signatures: { + "a.rbs" => <<~RBS + class Foo + def foo: [X < String?] (X) -> void + end + RBS + }, + code: { + "a.rb" => <<~RUBY + class Foo + def foo(x) + if x + x.encoding + end + + x.encoding + end + end + + foo = Foo.new + foo.foo("123") #$ String + foo.foo("foo") #$ String? + foo.foo(nil) + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: + - range: + start: + line: 7 + character: 6 + end: + line: 7 + character: 14 + severity: ERROR + message: Type `(::String | nil)` does not have method `encoding` + code: Ruby::NoMethod + YAML + ) + end end From 0857b6847c8315af2fc1502d0f8fa0454c7d2676 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Fri, 30 Aug 2024 14:55:54 +0900 Subject: [PATCH 05/11] Let all unbounded type params bounded by `Object?` --- lib/steep/interface/builder.rb | 2 +- lib/steep/interface/type_param.rb | 2 ++ lib/steep/subtyping/check.rb | 22 ++++++++++------------ lib/steep/type_construction.rb | 23 ++++++++++------------- sig/steep/interface/type_param.rbs | 6 ++++++ test/type_check_test.rb | 30 ++++++++++++++++++++++++++++++ 6 files changed, 59 insertions(+), 26 deletions(-) diff --git a/lib/steep/interface/builder.rb b/lib/steep/interface/builder.rb index 97869fa7e..82e6088c7 100644 --- a/lib/steep/interface/builder.rb +++ b/lib/steep/interface/builder.rb @@ -50,7 +50,7 @@ def validate_fvs(name, type) end def upper_bound(a) - variable_bounds.fetch(a, nil) + variable_bounds.fetch(a, AST::Builtin::Object.instance_type) end end diff --git a/lib/steep/interface/type_param.rb b/lib/steep/interface/type_param.rb index ad9a41391..e848f01a5 100644 --- a/lib/steep/interface/type_param.rb +++ b/lib/steep/interface/type_param.rb @@ -1,6 +1,8 @@ module Steep module Interface class TypeParam + IMPLICIT_UPPER_BOUND = AST::Builtin.optional(AST::Builtin::Object.instance_type) + attr_reader :name attr_reader :upper_bound attr_reader :variance diff --git a/lib/steep/subtyping/check.rb b/lib/steep/subtyping/check.rb index 21e0a31f7..7fdc8b0da 100644 --- a/lib/steep/subtyping/check.rb +++ b/lib/steep/subtyping/check.rb @@ -59,7 +59,7 @@ def push_variable_bounds(params) def variable_upper_bound(name) @bounds.reverse_each do |hash| if hash.key?(name) - return hash[name] + return hash.fetch(name) end end @@ -334,17 +334,14 @@ def check_type0(relation) end when relation.super_type.is_a?(AST::Types::Var) && constraints.unknown?(relation.super_type.name) - if ub = variable_upper_bound(relation.super_type.name) - Expand(relation) do - check_type(Relation.new(sub_type: relation.sub_type, super_type: ub)) - end.tap do |result| - if result.success? - constraints.add(relation.super_type.name, sub_type: relation.sub_type) - end + ub = variable_upper_bound(relation.super_type.name) || Interface::TypeParam::IMPLICIT_UPPER_BOUND + + Expand(relation) do + check_type(Relation.new(sub_type: relation.sub_type, super_type: ub)) + end.tap do |result| + if result.success? + constraints.add(relation.super_type.name, sub_type: relation.sub_type) end - else - constraints.add(relation.super_type.name, sub_type: relation.sub_type) - Success(relation) end when relation.sub_type.is_a?(AST::Types::Var) && constraints.unknown?(relation.sub_type.name) @@ -390,8 +387,9 @@ def check_type0(relation) end end - when relation.sub_type.is_a?(AST::Types::Var) && ub = variable_upper_bound(relation.sub_type.name) + when relation.sub_type.is_a?(AST::Types::Var) Expand(relation) do + ub = variable_upper_bound(relation.sub_type.name) || Interface::TypeParam::IMPLICIT_UPPER_BOUND check_type(Relation.new(sub_type: ub, super_type: relation.super_type)) end diff --git a/lib/steep/type_construction.rb b/lib/steep/type_construction.rb index 231d3100e..725bbde45 100644 --- a/lib/steep/type_construction.rb +++ b/lib/steep/type_construction.rb @@ -3846,20 +3846,17 @@ def try_method_type(node, receiver_type:, method_name:, method_overload:, argume type_args.each_with_index do |type, index| param = method_type.type_params[index] - if param.upper_bound - if result = no_subtyping?(sub_type: type.value, super_type: param.upper_bound) - args_ << AST::Builtin.any_type - constr.typing.add_error( - Diagnostic::Ruby::TypeArgumentMismatchError.new( - type_arg: type.value, - type_param: param, - result: result, - location: type.location - ) + upper_bound = param.upper_bound || Interface::TypeParam::IMPLICIT_UPPER_BOUND + if result = no_subtyping?(sub_type: type.value, super_type: upper_bound) + args_ << AST::Builtin.any_type + constr.typing.add_error( + Diagnostic::Ruby::TypeArgumentMismatchError.new( + type_arg: type.value, + type_param: param, + result: result, + location: type.location ) - else - args_ << type.value - end + ) else args_ << type.value end diff --git a/sig/steep/interface/type_param.rbs b/sig/steep/interface/type_param.rbs index bec7e0a57..06fa1f666 100644 --- a/sig/steep/interface/type_param.rbs +++ b/sig/steep/interface/type_param.rbs @@ -1,6 +1,12 @@ module Steep module Interface class TypeParam + # Implicit upper bound given to unqualified generic parameter + # + # It's `Object?` currently. + # + IMPLICIT_UPPER_BOUND: AST::Types::t + type loc = RBS::Location[untyped, untyped] type variance = RBS::AST::TypeParam::variance diff --git a/test/type_check_test.rb b/test/type_check_test.rb index dbceb60ca..795737a9d 100644 --- a/test/type_check_test.rb +++ b/test/type_check_test.rb @@ -2186,4 +2186,34 @@ def foo(x) YAML ) end + + def test_generics_upperbound_implicitly_object + run_type_check_test( + signatures: { + "a.rbs" => <<~RBS + class Foo + def foo: [X] (X) -> void + end + RBS + }, + code: { + "a.rb" => <<~RUBY + class Foo + def foo(x) + x.nil? + end + end + + foo = Foo.new + foo.foo("") + foo.foo(NilClass.new) + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: [] + YAML + ) + end end From 519b6056447d45a87b9e4544fbbd86598756c3d8 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Fri, 6 Sep 2024 14:42:09 +0900 Subject: [PATCH 06/11] Support default types of generic params --- lib/steep/ast/types/factory.rb | 32 ++++++++++++++++++++++---- lib/steep/interface/builder.rb | 10 ++++---- lib/steep/interface/type_param.rb | 27 ++++++++++++++++------ lib/steep/signature/validator.rb | 3 ++- lib/steep/type_construction.rb | 6 +++-- sig/steep/ast/types/factory.rbs | 2 ++ sig/steep/interface/type_param.rbs | 6 +++-- test/subtyping_test.rb | 3 ++- test/type_check_test.rb | 37 ++++++++++++++++++++++++++++++ 9 files changed, 104 insertions(+), 22 deletions(-) diff --git a/lib/steep/ast/types/factory.rb b/lib/steep/ast/types/factory.rb index 9b133c02a..20aeccca7 100644 --- a/lib/steep/ast/types/factory.rb +++ b/lib/steep/ast/types/factory.rb @@ -36,6 +36,29 @@ def type_1_opt(type) end end + def normalize_args(type_name, args) + case + when type_name.class? + if entry = env.normalized_module_class_entry(type_name) + type_params = entry.type_params + end + when type_name.interface? + if entry = env.interface_decls.fetch(type_name, nil) + type_params = entry.decl.type_params + end + when type_name.alias? + if entry = env.type_alias_decls.fetch(type_name, nil) + type_params = entry.decl.type_params + end + end + + if type_params && !type_params.empty? + RBS::AST::TypeParam.normalize_args(type_params, args) + else + args + end + end + def type(type) if ty = type_cache[type] return ty @@ -68,15 +91,15 @@ def type(type) Name::Singleton.new(name: type_name) when RBS::Types::ClassInstance type_name = type.name - args = type.args.map {|arg| type(arg) } + args = normalize_args(type_name, type.args).map {|arg| type(arg) } Name::Instance.new(name: type_name, args: args) when RBS::Types::Interface type_name = type.name - args = type.args.map {|arg| type(arg) } + args = normalize_args(type_name, type.args).map {|arg| type(arg) } Name::Interface.new(name: type_name, args: args) when RBS::Types::Alias type_name = type.name - args = type.args.map {|arg| type(arg) } + args = normalize_args(type_name, type.args).map {|arg| type(arg) } Name::Alias.new(name: type_name, args: args) when RBS::Types::Union Union.build(types: type.types.map {|ty| type(ty) }) @@ -247,7 +270,8 @@ def type_param(type_param) name: type_param.name, upper_bound: type_opt(type_param.upper_bound_type), variance: type_param.variance, - unchecked: type_param.unchecked? + unchecked: type_param.unchecked?, + default_type: type_opt(type_param.default_type) ) end diff --git a/lib/steep/interface/builder.rb b/lib/steep/interface/builder.rb index 82e6088c7..486c284dc 100644 --- a/lib/steep/interface/builder.rb +++ b/lib/steep/interface/builder.rb @@ -50,7 +50,7 @@ def validate_fvs(name, type) end def upper_bound(a) - variable_bounds.fetch(a, AST::Builtin::Object.instance_type) + variable_bounds.fetch(a, Interface::TypeParam::IMPLICIT_UPPER_BOUND) end end @@ -494,7 +494,7 @@ def tuple_shape(tuple) block: nil ), MethodType.new( - type_params: [TypeParam.new(name: :T, upper_bound: nil, variance: :invariant, unchecked: false)], + type_params: [TypeParam.new(name: :T, upper_bound: nil, variance: :invariant, unchecked: false, default_type: nil)], type: Function.new( params: Function::Params.build( required: [ @@ -508,7 +508,7 @@ def tuple_shape(tuple) block: nil ), MethodType.new( - type_params: [TypeParam.new(name: :T, upper_bound: nil, variance: :invariant, unchecked: false)], + type_params: [TypeParam.new(name: :T, upper_bound: nil, variance: :invariant, unchecked: false, default_type: nil)], type: Function.new( params: Function::Params.build(required: [AST::Types::Literal.new(value: index)]), return_type: AST::Types::Union.build(types: [elem_type, AST::Types::Var.new(name: :T)]), @@ -658,7 +658,7 @@ def record_shape(record) block: nil ), MethodType.new( - type_params: [TypeParam.new(name: :T, upper_bound: nil, variance: :invariant, unchecked: false)], + type_params: [TypeParam.new(name: :T, upper_bound: nil, variance: :invariant, unchecked: false, default_type: nil)], type: Function.new( params: Function::Params.build(required: [key_type, AST::Types::Var.new(name: :T)]), return_type: AST::Types::Union.build(types: [value_type, AST::Types::Var.new(name: :T)]), @@ -667,7 +667,7 @@ def record_shape(record) block: nil ), MethodType.new( - type_params: [TypeParam.new(name: :T, upper_bound: nil, variance: :invariant, unchecked: false)], + type_params: [TypeParam.new(name: :T, upper_bound: nil, variance: :invariant, unchecked: false, default_type: nil)], type: Function.new( params: Function::Params.build(required: [key_type]), return_type: AST::Types::Union.build(types: [value_type, AST::Types::Var.new(name: :T)]), diff --git a/lib/steep/interface/type_param.rb b/lib/steep/interface/type_param.rb index e848f01a5..486f2377f 100644 --- a/lib/steep/interface/type_param.rb +++ b/lib/steep/interface/type_param.rb @@ -8,13 +8,15 @@ class TypeParam attr_reader :variance attr_reader :unchecked attr_reader :location + attr_reader :default_type - def initialize(name:, upper_bound:, variance:, unchecked:, location: nil) + def initialize(name:, upper_bound:, variance:, unchecked:, location: nil, default_type:) @name = name @upper_bound = upper_bound @variance = variance @unchecked = unchecked @location = location + @default_type = default_type end def ==(other) @@ -22,13 +24,14 @@ def ==(other) other.name == name && other.upper_bound == upper_bound && other.variance == variance && - other.unchecked == unchecked + other.unchecked == unchecked && + other.default_type == default_type end alias eql? == def hash - name.hash ^ upper_bound.hash ^ variance.hash ^ unchecked.hash + name.hash ^ upper_bound.hash ^ variance.hash ^ unchecked.hash ^ default_type.hash end def self.rename(params, conflicting_names = params.map(&:name), new_names = conflicting_names.map {|n| AST::Types::Var.fresh_name(n) }) @@ -46,7 +49,8 @@ def self.rename(params, conflicting_names = params.map(&:name), new_names = conf upper_bound: param.upper_bound&.subst(subst), variance: param.variance, unchecked: param.unchecked, - location: param.location + location: param.location, + default_type: param.default_type&.subst(subst) ) else param @@ -82,19 +86,28 @@ def to_s buf end - def update(name: self.name, upper_bound: self.upper_bound, variance: self.variance, unchecked: self.unchecked, location: self.location) + def update(name: self.name, upper_bound: self.upper_bound, variance: self.variance, unchecked: self.unchecked, location: self.location, default_type: self.default_type) TypeParam.new( name: name, upper_bound: upper_bound, variance: variance, unchecked: unchecked, - location: location + location: location, + default_type: default_type ) end def subst(s) if u = upper_bound - update(upper_bound: u.subst(s)) + ub = u.subst(s) + end + + if d = default_type + dt = d.subst(s) + end + + if ub || dt + update(upper_bound: ub, default_type: dt) else self end diff --git a/lib/steep/signature/validator.rb b/lib/steep/signature/validator.rb index a0a8c19fd..3c47166d4 100644 --- a/lib/steep/signature/validator.rb +++ b/lib/steep/signature/validator.rb @@ -101,7 +101,8 @@ def validate_type_application_constraints(type_name, type_params, type_args, loc name: param.name, upper_bound: upper_bound_type, variance: param.variance, - unchecked: param.unchecked? + unchecked: param.unchecked?, + default_type: factory.type_opt(param.default_type) ), location: location ) diff --git a/lib/steep/type_construction.rb b/lib/steep/type_construction.rb index 725bbde45..b3ee5cadf 100644 --- a/lib/steep/type_construction.rb +++ b/lib/steep/type_construction.rb @@ -407,7 +407,8 @@ def for_module(node, new_module_name) name: param.name, upper_bound: checker.factory.type_opt(param.upper_bound_type), variance: param.variance, - unchecked: param.unchecked? + unchecked: param.unchecked?, + default_type: checker.factory.type_opt(param.default_type) ) end variable_context = TypeInference::Context::TypeVariableContext.new(type_params) @@ -497,7 +498,8 @@ def for_class(node, new_class_name, super_class_name) upper_bound: type_param.upper_bound_type&.yield_self {|t| checker.factory.type(t) }, variance: type_param.variance, unchecked: type_param.unchecked?, - location: type_param.location + location: type_param.location, + default_type: checker.factory.type_opt(type_param.default_type) ) end variable_context = TypeInference::Context::TypeVariableContext.new(type_params) diff --git a/sig/steep/ast/types/factory.rbs b/sig/steep/ast/types/factory.rbs index 42f9f6d8b..f085e9407 100644 --- a/sig/steep/ast/types/factory.rbs +++ b/sig/steep/ast/types/factory.rbs @@ -20,6 +20,8 @@ module Steep def type_name_resolver: () -> TypeNameResolver + def normalize_args: (RBS::TypeName type_name, Array[RBS::Types::t]) -> Array[RBS::Types::t] + def type: (RBS::Types::t `type`) -> t def type_opt: (RBS::Types::t? `type`) -> t? diff --git a/sig/steep/interface/type_param.rbs b/sig/steep/interface/type_param.rbs index 06fa1f666..b08edac40 100644 --- a/sig/steep/interface/type_param.rbs +++ b/sig/steep/interface/type_param.rbs @@ -21,7 +21,9 @@ module Steep attr_reader location: loc? - def initialize: (name: Symbol, upper_bound: AST::Types::t?, variance: variance, unchecked: bool, ?location: loc?) -> void + attr_reader default_type: AST::Types::t? + + def initialize: (name: Symbol, upper_bound: AST::Types::t?, variance: variance, unchecked: bool, ?location: loc?, default_type: AST::Types::t?) -> void def ==: (untyped other) -> bool @@ -41,7 +43,7 @@ module Steep def to_s: () -> String - def update: (?name: Symbol, ?upper_bound: AST::Types::t?, ?variance: variance, ?unchecked: bool, ?location: loc?) -> TypeParam + def update: (?name: Symbol, ?upper_bound: AST::Types::t?, ?variance: variance, ?unchecked: bool, ?location: loc?, ?default_type: AST::Types::t?) -> TypeParam def subst: (Substitution s) -> TypeParam end diff --git a/test/subtyping_test.rb b/test/subtyping_test.rb index 739be661e..0e5541148 100644 --- a/test/subtyping_test.rb +++ b/test/subtyping_test.rb @@ -1009,7 +1009,8 @@ def type_params(checker, **params) name: name, upper_bound: upper_bound, variance: :invariant, - unchecked: false + unchecked: false, + default_type: nil ) end end diff --git a/test/type_check_test.rb b/test/type_check_test.rb index 795737a9d..676702bc0 100644 --- a/test/type_check_test.rb +++ b/test/type_check_test.rb @@ -1433,6 +1433,8 @@ def foo: () -> void def test_type_assertion__type_error run_type_check_test( signatures: { + "a.rbs" => <<~RBS + RBS }, code: { "a.rb" => <<~RUBY @@ -2216,4 +2218,39 @@ def foo(x) YAML ) end + + def test_generics_upperbound_default + run_type_check_test( + signatures: { + "a.rbs" => <<~RBS + class Foo[X = Integer] + def foo: (X) -> X + end + RBS + }, + code: { + "a.rb" => <<~RUBY + class Foo + def foo(x) + x + end + end + + x = Foo.new + x.foo(1) + 1 + + y = Foo.new #: Foo[String] + y.foo("foo") + "" + + z = Foo.new #: Foo + z.foo(1) + 1 + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: [] + YAML + ) + end end From bcd40b5f4f88ef020bfece1aa9625ea5d96b7dc2 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Mon, 9 Sep 2024 22:12:38 +0900 Subject: [PATCH 07/11] Fix type_construction_test.rb --- test/type_construction_test.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/type_construction_test.rb b/test/type_construction_test.rb index 08c34bb2b..94f8bce3e 100644 --- a/test/type_construction_test.rb +++ b/test/type_construction_test.rb @@ -4883,7 +4883,7 @@ def foo(x) with_standard_construction(checker, source) do |construction, typing| construction.synthesize(source.node) - assert_empty typing.errors + assert_no_error typing end end end @@ -6352,7 +6352,12 @@ def yield_self: [A] () { () -> A } -> A with_standard_construction(checker, source) do |construction, typing| construction.synthesize(source.node) - assert_equal 1, typing.errors.size + + assert_typing_error(typing, size: 1) do |errors| + assert_any!(errors) do |error| + assert_instance_of Diagnostic::Ruby::NoMethod, error + end + end end end end From e7a2ec3b5707d96730d6ba066e3b3a5fd65eab7a Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Mon, 9 Sep 2024 22:11:56 +0900 Subject: [PATCH 08/11] Move type variable expansion before Union/Intersection --- lib/steep/subtyping/check.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/steep/subtyping/check.rb b/lib/steep/subtyping/check.rb index 7fdc8b0da..e62d2ca12 100644 --- a/lib/steep/subtyping/check.rb +++ b/lib/steep/subtyping/check.rb @@ -348,6 +348,15 @@ def check_type0(relation) constraints.add(relation.sub_type.name, super_type: relation.super_type) Success(relation) + when relation.sub_type.is_a?(AST::Types::Var) + Expand(relation) do + ub = variable_upper_bound(relation.sub_type.name) || Interface::TypeParam::IMPLICIT_UPPER_BOUND + check_type(Relation.new(sub_type: ub, super_type: relation.super_type)) + end + + when relation.super_type.is_a?(AST::Types::Var) || relation.sub_type.is_a?(AST::Types::Var) + Failure(relation, Result::Failure::UnknownPairError.new(relation: relation)) + when relation.sub_type.is_a?(AST::Types::Union) All(relation) do |result| relation.sub_type.types.each do |sub_type| @@ -387,15 +396,6 @@ def check_type0(relation) end end - when relation.sub_type.is_a?(AST::Types::Var) - Expand(relation) do - ub = variable_upper_bound(relation.sub_type.name) || Interface::TypeParam::IMPLICIT_UPPER_BOUND - check_type(Relation.new(sub_type: ub, super_type: relation.super_type)) - end - - when relation.super_type.is_a?(AST::Types::Var) || relation.sub_type.is_a?(AST::Types::Var) - Failure(relation, Result::Failure::UnknownPairError.new(relation: relation)) - when relation.super_type.is_a?(AST::Types::Name::Interface) Expand(relation) do check_interface( From 4c63df9e004df43a23824856eadafb31bf21a374 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Mon, 9 Sep 2024 14:05:57 +0900 Subject: [PATCH 09/11] Logic type is a boolean --- lib/steep/subtyping/check.rb | 5 +---- test/subtyping_test.rb | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/steep/subtyping/check.rb b/lib/steep/subtyping/check.rb index e62d2ca12..e77d3209d 100644 --- a/lib/steep/subtyping/check.rb +++ b/lib/steep/subtyping/check.rb @@ -265,9 +265,6 @@ def check_type0(relation) when relation.sub_type.is_a?(AST::Types::Bot) success(relation) - when relation.sub_type.is_a?(AST::Types::Logic::Base) && (true_type?(relation.super_type) || false_type?(relation.super_type)) - success(relation) - when relation.super_type.is_a?(AST::Types::Boolean) Expand(relation) do check_type( @@ -278,7 +275,7 @@ def check_type0(relation) ) end - when relation.sub_type.is_a?(AST::Types::Boolean) + when relation.sub_type.is_a?(AST::Types::Boolean) || relation.sub_type.is_a?(AST::Types::Logic::Base) Expand(relation) do check_type( Relation.new( diff --git a/test/subtyping_test.rb b/test/subtyping_test.rb index 0e5541148..93f44e864 100644 --- a/test/subtyping_test.rb +++ b/test/subtyping_test.rb @@ -934,10 +934,20 @@ def test_literal_alias_union def test_logic_type with_checker do |checker| type = AST::Types::Logic::ReceiverIsNil.new() - assert_success_check checker, type, "true" - assert_success_check checker, type, "false" - assert_success_check checker, type, "::TrueClass" - assert_success_check checker, type, "::FalseClass" + + assert_success_check checker, type, "bool" + assert_success_check checker, type, "Object" + assert_success_check checker, type, "Object?" + + assert_fail_check checker, type, "true" + assert_fail_check checker, type, "false" + assert_fail_check checker, type, "::TrueClass" + assert_fail_check checker, type, "::FalseClass" + + assert_fail_check checker, "true", type + assert_fail_check checker, "false", type + assert_fail_check checker, "::TrueClass", type + assert_fail_check checker, "::FalseClass", type end end From 375dc63a8188b8ea6d9e3d1c431e427d61b1c86b Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Mon, 9 Sep 2024 22:11:19 +0900 Subject: [PATCH 10/11] Union/intersection subtyping order --- lib/steep/subtyping/check.rb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/steep/subtyping/check.rb b/lib/steep/subtyping/check.rb index e77d3209d..a99a255b4 100644 --- a/lib/steep/subtyping/check.rb +++ b/lib/steep/subtyping/check.rb @@ -354,20 +354,19 @@ def check_type0(relation) when relation.super_type.is_a?(AST::Types::Var) || relation.sub_type.is_a?(AST::Types::Var) Failure(relation, Result::Failure::UnknownPairError.new(relation: relation)) - when relation.sub_type.is_a?(AST::Types::Union) + when relation.super_type.is_a?(AST::Types::Intersection) All(relation) do |result| - relation.sub_type.types.each do |sub_type| - rel = Relation.new(sub_type: sub_type, super_type: relation.super_type) - result.add(rel) do + relation.super_type.types.each do |super_type| + result.add(Relation.new(sub_type: relation.sub_type, super_type: super_type)) do |rel| check_type(rel) end end end - when relation.super_type.is_a?(AST::Types::Union) - Any(relation) do |result| - relation.super_type.types.sort_by {|ty| (path = hole_path(ty)) ? -path.size : -Float::INFINITY }.each do |super_type| - rel = Relation.new(sub_type: relation.sub_type, super_type: super_type) + when relation.sub_type.is_a?(AST::Types::Union) + All(relation) do |result| + relation.sub_type.types.each do |sub_type| + rel = Relation.new(sub_type: sub_type, super_type: relation.super_type) result.add(rel) do check_type(rel) end @@ -384,10 +383,11 @@ def check_type0(relation) end end - when relation.super_type.is_a?(AST::Types::Intersection) - All(relation) do |result| - relation.super_type.types.each do |super_type| - result.add(Relation.new(sub_type: relation.sub_type, super_type: super_type)) do |rel| + when relation.super_type.is_a?(AST::Types::Union) + Any(relation) do |result| + relation.super_type.types.sort_by {|ty| (path = hole_path(ty)) ? -path.size : -Float::INFINITY }.each do |super_type| + rel = Relation.new(sub_type: relation.sub_type, super_type: super_type) + result.add(rel) do check_type(rel) end end From 9ec57bb367c6e8ab592da7c810295bdcb2760932 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Mon, 9 Sep 2024 23:00:42 +0900 Subject: [PATCH 11/11] Fix smoke tests Changing type of `Array#sample` caused the problems. --- smoke/and/a.rb | 2 +- smoke/diagnostics/else_on_exhaustive_case.rb | 2 +- smoke/diagnostics/unsupported_syntax.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/smoke/and/a.rb b/smoke/and/a.rb index 6f317224a..844f1bc64 100644 --- a/smoke/and/a.rb +++ b/smoke/and/a.rb @@ -1,7 +1,7 @@ # @type var b: String # @type var c: ::Integer -a = ["foo"].sample +a = ["foo"].find { true } b = a && a.to_str diff --git a/smoke/diagnostics/else_on_exhaustive_case.rb b/smoke/diagnostics/else_on_exhaustive_case.rb index 2346863a7..c86e4f9d2 100644 --- a/smoke/diagnostics/else_on_exhaustive_case.rb +++ b/smoke/diagnostics/else_on_exhaustive_case.rb @@ -1,4 +1,4 @@ -x = [1, ""].sample +x = [1, ""].find { true } case x when Integer diff --git a/smoke/diagnostics/unsupported_syntax.rb b/smoke/diagnostics/unsupported_syntax.rb index e6911491c..edfed9260 100644 --- a/smoke/diagnostics/unsupported_syntax.rb +++ b/smoke/diagnostics/unsupported_syntax.rb @@ -1,2 +1,2 @@ -class <<[1, ""].sample +class <<([1, ""].find { true }) end