From e1b6e448ed644621552c8fd8987bb0b1eace42da Mon Sep 17 00:00:00 2001 From: dak2 Date: Mon, 6 Apr 2026 23:02:42 +0900 Subject: [PATCH] Propagate block return types to method return types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Methods like `map` return generic types parameterized by block return values (e.g., `[1,2].map { |x| x.to_s }` → `Array[String]`). Without this, the type checker could not infer the element type of collections produced by block-yielding methods, causing false positives on subsequent method calls. Co-Authored-By: Claude Opus 4.6 (1M context) --- core/src/analyzer/blocks.rs | 22 +- core/src/analyzer/dispatch.rs | 40 ++-- core/src/cache/rbs_cache.rs | 9 +- core/src/checker.rs | 2 +- core/src/graph/box.rs | 364 ++++++++++++++++++++++++++++++++-- core/src/graph/mod.rs | 2 +- core/src/rbs/converter.rs | 107 +++++++++- test/block_test.rb | 67 +++++++ test/range_test.rb | 2 +- 9 files changed, 570 insertions(+), 45 deletions(-) diff --git a/core/src/analyzer/blocks.rs b/core/src/analyzer/blocks.rs index 3b0137d..805e3ed 100644 --- a/core/src/analyzer/blocks.rs +++ b/core/src/analyzer/blocks.rs @@ -11,6 +11,12 @@ use crate::graph::{ChangeSet, VertexId}; use super::bytes_to_name; use super::parameters::{install_optional_parameter, install_required_parameter, install_rest_parameter}; +/// Block processing result +pub(crate) struct BlockResult { + pub param_vtxs: Vec, + pub body_last_vtx: Option, +} + /// Process block node pub(crate) fn process_block_node( genv: &mut GlobalEnv, @@ -23,14 +29,14 @@ pub(crate) fn process_block_node( None } -/// Process block node and return block parameter vertex IDs +/// Process block node and return block parameter vertex IDs and body's last expression vertex pub(crate) fn process_block_node_with_params( genv: &mut GlobalEnv, lenv: &mut LocalEnv, changes: &mut ChangeSet, source: &str, block_node: &ruby_prism::BlockNode, -) -> Vec { +) -> BlockResult { enter_block_scope(genv); let mut param_vtxs = Vec::new(); @@ -42,17 +48,19 @@ pub(crate) fn process_block_node_with_params( } } - if let Some(body) = block_node.body() { + let body_last_vtx = if let Some(body) = block_node.body() { if let Some(statements) = body.as_statements_node() { - super::install::install_statements(genv, lenv, changes, source, &statements); + super::install::install_statements(genv, lenv, changes, source, &statements) } else { - super::install::install_node(genv, lenv, changes, source, &body); + super::install::install_node(genv, lenv, changes, source, &body) } - } + } else { + None + }; exit_block_scope(genv); - param_vtxs + BlockResult { param_vtxs, body_last_vtx } } /// Install block parameters and return their vertex IDs diff --git a/core/src/analyzer/dispatch.rs b/core/src/analyzer/dispatch.rs index 406443d..4db3914 100644 --- a/core/src/analyzer/dispatch.rs +++ b/core/src/analyzer/dispatch.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use crate::env::{GlobalEnv, LocalEnv}; -use crate::graph::{BlockParameterTypeBox, ChangeSet, VertexId}; +use crate::graph::{BlockParameterTypeBox, BlockReturnTypeBox, ChangeSet, VertexId}; use crate::source_map::SourceLocation; use crate::types::Type; use ruby_prism::Node; @@ -558,34 +558,48 @@ fn process_method_call_common<'a>( let (positional_arg_vtxs, kwarg_vtxs) = collect_arguments(genv, lenv, changes, source, arguments.into_iter()); + let ret_vtx = finish_method_call( + genv, + recv_vtx, + method_name.clone(), + positional_arg_vtxs, + kwarg_vtxs, + location, + safe_navigation, + ); + if let Some(block_node) = block { if let Some(block) = block_node.as_block_node() { - let param_vtxs = super::blocks::process_block_node_with_params( + let block_result = super::blocks::process_block_node_with_params( genv, lenv, changes, source, &block, ); - if !param_vtxs.is_empty() { + if !block_result.param_vtxs.is_empty() { let box_id = genv.alloc_box_id(); let block_box = BlockParameterTypeBox::new( box_id, recv_vtx, method_name.clone(), - param_vtxs, + block_result.param_vtxs, ); genv.register_box(box_id, Box::new(block_box)); } + + if let Some(body_vtx) = block_result.body_last_vtx { + let box_id = genv.alloc_box_id(); + let ret_box = BlockReturnTypeBox::new( + box_id, + recv_vtx, + method_name, + body_vtx, + ret_vtx, + ); + genv.register_box(box_id, Box::new(ret_box)); + } } } - Some(finish_method_call( - genv, - recv_vtx, - method_name, - positional_arg_vtxs, - kwarg_vtxs, - location, - safe_navigation, - )) + Some(ret_vtx) } /// Finish method call after receiver is processed diff --git a/core/src/cache/rbs_cache.rs b/core/src/cache/rbs_cache.rs index 2af847f..154098e 100644 --- a/core/src/cache/rbs_cache.rs +++ b/core/src/cache/rbs_cache.rs @@ -30,12 +30,7 @@ pub struct SerializableMethodInfo { pub block_param_types: Option>, } -impl SerializableMethodInfo { - /// Parse return type string into Type (simple parser for cached data) - pub fn return_type(&self) -> crate::types::Type { - crate::types::Type::instance(&self.return_type_str) - } -} + impl RbsCache { /// Get user cache file path (in ~/.cache/methodray/) @@ -201,7 +196,7 @@ mod tests { block_param_types: None, }; - let return_type = method_info.return_type(); + let return_type = crate::rbs::converter::RbsTypeConverter::parse(&method_info.return_type_str); assert_eq!(return_type.show(), "String"); } diff --git a/core/src/checker.rs b/core/src/checker.rs index 35876d2..a97688f 100644 --- a/core/src/checker.rs +++ b/core/src/checker.rs @@ -88,7 +88,7 @@ fn load_rbs_from_cache(genv: &mut GlobalEnv) -> Result<()> { genv.register_builtin_method_with_block( receiver_type, &method_info.method_name, - method_info.return_type(), + RbsTypeConverter::parse(&method_info.return_type_str), block_param_types, ); } diff --git a/core/src/graph/box.rs b/core/src/graph/box.rs index 2a2dd96..8b8f627 100644 --- a/core/src/graph/box.rs +++ b/core/src/graph/box.rs @@ -45,6 +45,75 @@ fn propagate_keyword_arguments( } } +/// Receiver type variables are resolved by position-matching against the receiver's Generic type_args. +fn is_type_variable_name(name: &str) -> bool { + matches!( + name, + "Elem" | "K" | "V" | "T" | "Element" | "Key" | "Value" + ) +} + +/// Block output type variables (e.g., U in `map { -> U }`) cannot be resolved from the receiver +/// and are substituted by BlockReturnTypeBox using the block body's return type. +fn is_block_type_variable_name(name: &str) -> bool { + matches!(name, "U" | "A" | "B" | "Out" | "In") +} + +/// Resolve type variables in a return type using the receiver's type args. +/// +/// When `block_return_type` is None, block type variables cause None to be returned +/// (deferring to BlockReturnTypeBox). When provided, block type variables are +/// substituted with the given type. +fn resolve_return_type( + return_type: &Type, + recv_ty: &Type, + block_return_type: Option<&Type>, +) -> Option { + match return_type { + Type::Instance { name } if is_block_type_variable_name(name.full_name()) => { + block_return_type.cloned() + } + Type::Instance { name } if is_type_variable_name(name.full_name()) => { + BlockParameterTypeBox::resolve_type_variable(return_type, recv_ty) + } + Type::Generic { name, type_args } => { + let mut resolved_args = Vec::with_capacity(type_args.len()); + for arg in type_args { + match arg { + Type::Instance { name: arg_name } + if is_block_type_variable_name(arg_name.full_name()) => + { + match block_return_type { + Some(brt) => resolved_args.push(brt.clone()), + None => return None, + } + } + Type::Instance { name: arg_name } + if is_type_variable_name(arg_name.full_name()) => + { + match BlockParameterTypeBox::resolve_type_variable(arg, recv_ty) { + Some(resolved) => resolved_args.push(resolved), + None => return None, + } + } + Type::Generic { .. } => { + match resolve_return_type(arg, recv_ty, block_return_type) { + Some(resolved) => resolved_args.push(resolved), + None => return None, + } + } + _ => resolved_args.push(arg.clone()), + } + } + Some(Type::Generic { + name: name.clone(), + type_args: resolved_args, + }) + } + _ => Some(return_type.clone()), + } +} + /// Box representing a method call pub struct MethodCallBox { id: BoxId, @@ -138,9 +207,13 @@ impl MethodCallBox { changes, ); } else { - // Builtin/RBS method: create source with fixed return type - let ret_src_id = genv.new_source(method_info.return_type.clone()); - changes.add_edge(ret_src_id, self.ret); + // Builtin/RBS method: resolve type variables from receiver's type args. + // If unresolvable variables remain (e.g., block's U in map → Array[U]), + // skip — BlockReturnTypeBox will add the resolved type. + if let Some(resolved) = resolve_return_type(&method_info.return_type, recv_ty, None) { + let ret_src_id = genv.new_source(resolved); + changes.add_edge(ret_src_id, self.ret); + } } } else if self.method_name == "new" { self.handle_new_call(recv_ty, genv, changes); @@ -251,14 +324,6 @@ impl BlockParameterTypeBox { } } - /// Check if a type is a type variable name (e.g., Elem, K, V) - fn is_type_variable_name(name: &str) -> bool { - matches!( - name, - "Elem" | "K" | "V" | "T" | "U" | "A" | "B" | "Element" | "Key" | "Value" | "Out" | "In" - ) - } - /// Try to resolve a type variable from receiver's type arguments. /// /// For `Array[Integer]#each { |x| }`, the block param type is `Elem`. @@ -267,9 +332,9 @@ impl BlockParameterTypeBox { /// Type variable mapping for common generic classes: /// - Array[Elem]: Elem → type_args[0] /// - Hash[K, V]: K → type_args[0], V → type_args[1] - fn resolve_type_variable(ty: &Type, recv_ty: &Type) -> Option { + pub(crate) fn resolve_type_variable(ty: &Type, recv_ty: &Type) -> Option { let type_var_name = match ty { - Type::Instance { name } if Self::is_type_variable_name(name.full_name()) => { + Type::Instance { name } if is_type_variable_name(name.full_name()) => { name.full_name() } _ => return None, // Not a type variable @@ -335,7 +400,9 @@ impl BoxTrait for BlockParameterTypeBox { // Type variable resolved (e.g., Elem → Integer) resolved } else if let Type::Instance { name } = ¶m_type { - if Self::is_type_variable_name(name.full_name()) { + if is_type_variable_name(name.full_name()) + || is_block_type_variable_name(name.full_name()) + { // Type variable couldn't be resolved, skip continue; } else { @@ -356,3 +423,272 @@ impl BoxTrait for BlockParameterTypeBox { } } } + +/// Box for propagating block return type to method's generic return type. +/// +/// For `[1,2].map { |x| x.to_s }`: +/// - Observes block body's return vertex type (String) +/// - Resolves method's RBS return type (Array[Elem]) +/// - Substitutes: Elem → block_return → Array[String] +/// - Adds edge to method's return vertex +pub struct BlockReturnTypeBox { + id: BoxId, + recv_vtx: VertexId, + method_name: String, + block_return_vtx: VertexId, + method_return_vtx: VertexId, + reschedule_count: u8, +} + +impl BlockReturnTypeBox { + pub fn new( + id: BoxId, + recv_vtx: VertexId, + method_name: String, + block_return_vtx: VertexId, + method_return_vtx: VertexId, + ) -> Self { + Self { + id, + recv_vtx, + method_name, + block_return_vtx, + method_return_vtx, + reschedule_count: 0, + } + } + + fn try_reschedule(&mut self, changes: &mut ChangeSet) { + if self.reschedule_count < MAX_RESCHEDULE_COUNT { + self.reschedule_count += 1; + changes.reschedule(self.id); + } + } +} + +impl BoxTrait for BlockReturnTypeBox { + fn id(&self) -> BoxId { + self.id + } + + fn ret(&self) -> VertexId { + self.method_return_vtx + } + + fn run(&mut self, genv: &mut GlobalEnv, changes: &mut ChangeSet) { + let Some(recv_types) = genv.get_receiver_types(self.recv_vtx) else { + return; + }; + if recv_types.is_empty() { + self.try_reschedule(changes); + return; + } + + let block_return_types = match genv.get_receiver_types(self.block_return_vtx) { + Some(types) if !types.is_empty() => types, + _ => { + self.try_reschedule(changes); + return; + } + }; + + let block_return_union = Type::union_of(block_return_types); + + for recv_ty in &recv_types { + let Some(info) = genv.resolve_method(recv_ty, &self.method_name) else { + continue; + }; + + // Reuse resolve_return_type with block_return_union to substitute + // both receiver type variables and block output type variables in one pass. + if let Some(resolved) = + resolve_return_type(&info.return_type, recv_ty, Some(&block_return_union)) + { + let src = genv.new_source(resolved); + changes.add_edge(src, self.method_return_vtx); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::QualifiedName; + + #[test] + fn test_is_type_variable_name() { + assert!(is_type_variable_name("Elem")); + assert!(is_type_variable_name("K")); + assert!(is_type_variable_name("V")); + assert!(is_type_variable_name("T")); + assert!(!is_type_variable_name("U")); + assert!(!is_type_variable_name("String")); + assert!(!is_type_variable_name("In")); + } + + #[test] + fn test_is_block_type_variable_name() { + assert!(is_block_type_variable_name("U")); + assert!(is_block_type_variable_name("A")); + assert!(is_block_type_variable_name("B")); + assert!(is_block_type_variable_name("Out")); + assert!(is_block_type_variable_name("In")); + assert!(!is_block_type_variable_name("Elem")); + assert!(!is_block_type_variable_name("String")); + } + + #[test] + fn test_resolve_return_type_passthrough() { + let recv = Type::instance("String"); + let ret = Type::instance("Integer"); + assert_eq!(resolve_return_type(&ret, &recv, None), Some(Type::instance("Integer"))); + } + + #[test] + fn test_resolve_return_type_receiver_type_variable() { + // Array[Integer]#first → Elem → Integer + let recv = Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("Integer")], + }; + let ret = Type::instance("Elem"); + assert_eq!(resolve_return_type(&ret, &recv, None), Some(Type::instance("Integer"))); + } + + #[test] + fn test_resolve_return_type_block_type_variable_returns_none() { + let recv = Type::instance("Array"); + let ret = Type::instance("U"); + assert_eq!(resolve_return_type(&ret, &recv, None), None); + } + + #[test] + fn test_resolve_return_type_generic_with_block_variable() { + // Array[U] should return None (U is a block type variable) + let recv = Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("Integer")], + }; + let ret = Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("U")], + }; + assert_eq!(resolve_return_type(&ret, &recv, None), None); + } + + #[test] + fn test_resolve_return_type_generic_with_resolvable_variable() { + // Array[Elem] with recv Array[String] → Array[String] + let recv = Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("String")], + }; + let ret = Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("Elem")], + }; + let expected = Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("String")], + }; + assert_eq!(resolve_return_type(&ret, &recv, None), Some(expected)); + } + + #[test] + fn test_resolve_return_type_nested_generic_resolvable() { + // Hash[K, Array[Elem]] with recv Hash[String, Integer] + // K → String (Hash mapping), Elem → String (generic fallback: index 0) + let recv = Type::Generic { + name: QualifiedName::simple("Hash"), + type_args: vec![Type::instance("String"), Type::instance("Integer")], + }; + let ret = Type::Generic { + name: QualifiedName::simple("Hash"), + type_args: vec![ + Type::instance("K"), + Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("Elem")], + }, + ], + }; + let expected = Type::Generic { + name: QualifiedName::simple("Hash"), + type_args: vec![ + Type::instance("String"), + Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("String")], + }, + ], + }; + assert_eq!(resolve_return_type(&ret, &recv, None), Some(expected)); + } + + #[test] + fn test_resolve_return_type_nested_generic_with_block_var() { + // Array[Array[U]] with recv Array[Integer] → None (U is block variable) + let recv = Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("Integer")], + }; + let ret = Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("U")], + }], + }; + assert_eq!(resolve_return_type(&ret, &recv, None), None); + } + + #[test] + fn test_resolve_return_type_block_variable_with_substitution() { + // U with block_return_type=String → String + let recv = Type::instance("Array"); + let ret = Type::instance("U"); + let brt = Type::instance("String"); + assert_eq!( + resolve_return_type(&ret, &recv, Some(&brt)), + Some(Type::instance("String")) + ); + } + + #[test] + fn test_resolve_return_type_generic_block_variable_with_substitution() { + // Array[U] with block_return_type=String → Array[String] + let recv = Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("Integer")], + }; + let ret = Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("U")], + }; + let brt = Type::instance("String"); + let expected = Type::Generic { + name: QualifiedName::simple("Array"), + type_args: vec![Type::instance("String")], + }; + assert_eq!(resolve_return_type(&ret, &recv, Some(&brt)), Some(expected)); + } + + #[test] + fn test_resolve_return_type_nested_generic_all_resolvable() { + // Hash[K, V] with recv Hash[String, Integer] → Hash[String, Integer] + let recv = Type::Generic { + name: QualifiedName::simple("Hash"), + type_args: vec![Type::instance("String"), Type::instance("Integer")], + }; + let ret = Type::Generic { + name: QualifiedName::simple("Hash"), + type_args: vec![Type::instance("K"), Type::instance("V")], + }; + let expected = Type::Generic { + name: QualifiedName::simple("Hash"), + type_args: vec![Type::instance("String"), Type::instance("Integer")], + }; + assert_eq!(resolve_return_type(&ret, &recv, None), Some(expected)); + } +} diff --git a/core/src/graph/mod.rs b/core/src/graph/mod.rs index 75cc966..a3e619d 100644 --- a/core/src/graph/mod.rs +++ b/core/src/graph/mod.rs @@ -3,5 +3,5 @@ pub mod change_set; pub mod vertex; pub use change_set::{ChangeSet, EdgeUpdate}; -pub use r#box::{BlockParameterTypeBox, BoxId, BoxTrait, MethodCallBox}; +pub use r#box::{BlockParameterTypeBox, BlockReturnTypeBox, BoxId, BoxTrait, MethodCallBox}; pub use vertex::{Source, Vertex, VertexId}; diff --git a/core/src/rbs/converter.rs b/core/src/rbs/converter.rs index 3df4c7a..3ad52ef 100644 --- a/core/src/rbs/converter.rs +++ b/core/src/rbs/converter.rs @@ -3,6 +3,27 @@ use crate::types::Type; // RBS Type Converter pub struct RbsTypeConverter; +/// Split type arguments by comma, respecting bracket nesting depth. +/// e.g., "String, Array[Integer]" → ["String", "Array[Integer]"] +fn split_type_args(s: &str) -> Vec<&str> { + let mut results = Vec::new(); + let mut depth = 0; + let mut start = 0; + for (i, b) in s.bytes().enumerate() { + match b { + b'[' => depth += 1, + b']' => depth -= 1, + b',' if depth == 0 => { + results.push(s[start..i].trim()); + start = i + 1; + } + _ => {} + } + } + results.push(s[start..].trim()); + results +} + impl RbsTypeConverter { pub fn parse(rbs_type: &str) -> Type { // Handle union types @@ -25,7 +46,25 @@ impl RbsTypeConverter { ]), "void" | "nil" => Type::Nil, "untyped" | "top" => Type::Bot, - _ => Type::instance(type_name), + _ => { + // Handle generic types: Array[Elem], Hash[K, V] + // Only parse as generic when base class name is non-empty (skip tuple-like `[...]`) + if let Some(bracket_start) = type_name.find('[') { + if bracket_start > 0 && type_name.ends_with(']') { + let base = &type_name[..bracket_start]; + let args_str = &type_name[bracket_start + 1..type_name.len() - 1]; + let type_args: Vec = split_type_args(args_str) + .into_iter() + .map(Self::parse) + .collect(); + return Type::Generic { + name: crate::types::QualifiedName::from(base), + type_args, + }; + } + } + Type::instance(type_name) + } } } } @@ -84,4 +123,70 @@ mod tests { _ => panic!("Expected Union type"), } } + + #[test] + fn test_parse_generic_single_arg() { + match RbsTypeConverter::parse("Array[Elem]") { + Type::Generic { name, type_args } => { + assert_eq!(name.full_name(), "Array"); + assert_eq!(type_args.len(), 1); + match &type_args[0] { + Type::Instance { name } => assert_eq!(name.full_name(), "Elem"), + _ => panic!("Expected Instance for type arg"), + } + } + _ => panic!("Expected Generic type"), + } + } + + #[test] + fn test_parse_generic_multiple_args() { + match RbsTypeConverter::parse("Hash[K, V]") { + Type::Generic { name, type_args } => { + assert_eq!(name.full_name(), "Hash"); + assert_eq!(type_args.len(), 2); + } + _ => panic!("Expected Generic type"), + } + } + + #[test] + fn test_parse_nested_generic() { + match RbsTypeConverter::parse("Hash[String, Array[Integer]]") { + Type::Generic { name, type_args } => { + assert_eq!(name.full_name(), "Hash"); + assert_eq!(type_args.len(), 2); + match &type_args[0] { + Type::Instance { name } => assert_eq!(name.full_name(), "String"), + _ => panic!("Expected Instance for first arg"), + } + match &type_args[1] { + Type::Generic { name, type_args } => { + assert_eq!(name.full_name(), "Array"); + assert_eq!(type_args.len(), 1); + } + _ => panic!("Expected Generic for second arg"), + } + } + _ => panic!("Expected Generic type"), + } + } + + #[test] + fn test_parse_bare_bracket_not_generic() { + // "[String]" should not be parsed as generic (bracket_start == 0) + match RbsTypeConverter::parse("[String]") { + Type::Instance { name } => assert_eq!(name.full_name(), "[String]"), + _ => panic!("Expected Instance type for bare bracket"), + } + } + + #[test] + fn test_split_type_args_nested() { + let result = split_type_args("String, Array[Integer]"); + assert_eq!(result, vec!["String", "Array[Integer]"]); + + let result = split_type_args("Array[K, V], Integer"); + assert_eq!(result, vec!["Array[K, V]", "Integer"]); + } } diff --git a/test/block_test.rb b/test/block_test.rb index c7986ee..210ac75 100644 --- a/test/block_test.rb +++ b/test/block_test.rb @@ -138,4 +138,71 @@ def process assert_check_error(source, method_name: 'upcase', receiver_type: 'Integer') end + + # ============================================ + # Block Return Type Propagation + # ============================================ + + def test_map_propagates_block_return_type + source = <<~RUBY + class Formatter + def format + result = [1, 2, 3].map { |x| x.to_s } + result.first.upcase + end + end + RUBY + + assert_no_check_errors(source) + end + + def test_map_with_integer_return + source = <<~RUBY + class Counter + def count + result = ["a", "b"].map { |x| x.length } + result.first.even? + end + end + RUBY + + assert_no_check_errors(source) + end + + def test_map_block_return_type_error + source = <<~RUBY + class Formatter + def format + result = [1, 2, 3].map { |x| x.to_s } + result.first.even? + end + end + RUBY + + assert_check_error(source, method_name: 'even?', receiver_type: 'String') + end + + def test_each_does_not_propagate_block_return_type + source = <<~RUBY + class Processor + def process + [1, 2, 3].each { |x| x.to_s } + end + end + RUBY + + assert_no_check_errors(source) + end + + def test_empty_block_body + source = <<~RUBY + class Processor + def process + [1, 2, 3].map { |x| } + end + end + RUBY + + assert_no_check_errors(source) + end end diff --git a/test/range_test.rb b/test/range_test.rb index 39efac1..229a0e5 100644 --- a/test/range_test.rb +++ b/test/range_test.rb @@ -27,7 +27,7 @@ def test_range_float def test_range_to_a_returns_array types = infer("x = 1..10\na = x.to_a") - assert_equal 'Array[Elem]', types['a'] + assert_equal 'Array[Integer]', types['a'] end def test_range_size_returns_integer