Skip to content
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
142 changes: 0 additions & 142 deletions core/src/analyzer/loops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,7 @@ pub(crate) fn process_for_node(
mod tests {
use crate::analyzer::install::AstInstaller;
use crate::env::{GlobalEnv, LocalEnv};
use crate::graph::VertexId;
use crate::parser::ParseSession;
use crate::types::Type;

/// Helper: parse Ruby source, process with AstInstaller, and return GlobalEnv
fn analyze(source: &str) -> GlobalEnv {
Expand All @@ -120,55 +118,6 @@ mod tests {
genv
}

/// Helper: get the type string for a vertex ID (checks both Vertex and Source)
fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
if let Some(vertex) = genv.get_vertex(vtx) {
vertex.show()
} else if let Some(source) = genv.get_source(vtx) {
source.ty.show()
} else {
panic!("vertex {:?} not found as either Vertex or Source", vtx);
}
}

#[test]
fn test_while_returns_nil() {
let source = r#"
class Foo
def bar
while true
"hello"
end
end
end
"#;
let genv = analyze(source);
let info = genv
.resolve_method(&Type::instance("Foo"), "bar")
.expect("Foo#bar should be registered");
let ret_vtx = info.return_vertex.unwrap();
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
}

#[test]
fn test_until_returns_nil() {
let source = r#"
class Foo
def bar
until false
"hello"
end
end
end
"#;
let genv = analyze(source);
let info = genv
.resolve_method(&Type::instance("Foo"), "bar")
.expect("Foo#bar should be registered");
let ret_vtx = info.return_vertex.unwrap();
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
}

#[test]
fn test_while_variable_assignment_in_body() {
// Should not panic — variable assignment inside loop is processed
Expand All @@ -181,63 +130,6 @@ end
analyze(source);
}

#[test]
fn test_while_modifier_form() {
let source = r#"
class Foo
def bar
x = "hello" while false
end
end
"#;
let genv = analyze(source);
let info = genv
.resolve_method(&Type::instance("Foo"), "bar")
.expect("Foo#bar should be registered");
let ret_vtx = info.return_vertex.unwrap();
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
}

#[test]
fn test_begin_end_while() {
let source = r#"
class Foo
def bar
begin
"hello"
end while false
end
end
"#;
let genv = analyze(source);
let info = genv
.resolve_method(&Type::instance("Foo"), "bar")
.expect("Foo#bar should be registered");
let ret_vtx = info.return_vertex.unwrap();
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
}

// --- for loop tests ---

#[test]
fn test_for_returns_nil() {
let source = r#"
class Foo
def bar
for x in [1, 2, 3]
x
end
end
end
"#;
let genv = analyze(source);
let info = genv
.resolve_method(&Type::instance("Foo"), "bar")
.expect("Foo#bar should be registered");
let ret_vtx = info.return_vertex.unwrap();
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
}

#[test]
fn test_for_variable_type_from_array() {
let source = r#"
Expand All @@ -264,38 +156,4 @@ end
// Should not panic — x is accessible after the loop
analyze(source);
}

#[test]
fn test_for_empty_body() {
let source = r#"
class Foo
def bar
for x in [1, 2, 3]
end
end
end
"#;
let genv = analyze(source);
let info = genv
.resolve_method(&Type::instance("Foo"), "bar")
.expect("Foo#bar should be registered");
let ret_vtx = info.return_vertex.unwrap();
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
}

#[test]
fn test_for_with_method_call_in_body() {
// Should not panic — method call on loop variable is processed
// (type error check requires RBS, covered by Ruby integration test)
let source = r#"
class Foo
def bar
for item in ["hello", "world"]
item.upcase
end
end
end
"#;
analyze(source);
}
}
152 changes: 0 additions & 152 deletions core/src/analyzer/returns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,155 +37,3 @@ pub(crate) fn process_return_node(

None
}

#[cfg(test)]
mod tests {
use crate::env::{GlobalEnv, LocalEnv};
use crate::graph::ChangeSet;
use crate::parser::ParseSession;
use crate::types::Type;

fn setup_and_infer(source: &str) -> GlobalEnv {
let session = ParseSession::new();
let parse_result = session.parse_source(source, "test.rb").unwrap();
let root = parse_result.node();
let program = root.as_program_node().unwrap();

let mut genv = GlobalEnv::new();
let mut lenv = LocalEnv::new();
let mut changes = ChangeSet::new();

for stmt in &program.statements().body() {
crate::analyzer::install::install_node(
&mut genv, &mut lenv, &mut changes, source, &stmt,
);
}

genv.apply_changes(changes);
genv.run_all();
genv
}

fn get_return_type(genv: &GlobalEnv, class_name: &str, method_name: &str) -> String {
let info = genv
.resolve_method(&Type::instance(class_name), method_name)
.unwrap_or_else(|| panic!("{}#{} should be registered", class_name, method_name));
let vtx = info
.return_vertex
.expect("return_vertex should be Some");

if let Some(source) = genv.get_source(vtx) {
source.ty.show()
} else if let Some(vertex) = genv.get_vertex(vtx) {
vertex.show()
} else {
panic!("return_vertex not found");
}
}

#[test]
fn test_simple_return() {
let source = r#"
class Foo
def bar
return "hello"
end
end
"#;
let genv = setup_and_infer(source);
assert_eq!(get_return_type(&genv, "Foo", "bar"), "String");
}

#[test]
fn test_return_with_implicit_return_union() {
let source = r#"
class Foo
def bar
return "hello" if true
42
end
end
"#;
let genv = setup_and_infer(source);
let ty = get_return_type(&genv, "Foo", "bar");
assert!(ty.contains("Integer"), "should contain Integer, got: {}", ty);
assert!(ty.contains("String"), "should contain String, got: {}", ty);
}

#[test]
fn test_multiple_returns() {
let source = r#"
class Foo
def bar
return "a" if true
return :b if false
42
end
end
"#;
let genv = setup_and_infer(source);
let ty = get_return_type(&genv, "Foo", "bar");
assert!(ty.contains("Integer"), "should contain Integer, got: {}", ty);
assert!(ty.contains("String"), "should contain String, got: {}", ty);
assert!(ty.contains("Symbol"), "should contain Symbol, got: {}", ty);
}

#[test]
fn test_return_without_value() {
let source = r#"
class Foo
def bar
return if true
42
end
end
"#;
let genv = setup_and_infer(source);
let ty = get_return_type(&genv, "Foo", "bar");
assert!(ty.contains("Integer"), "should contain Integer, got: {}", ty);
assert!(ty.contains("nil"), "should contain nil, got: {}", ty);
}

#[test]
fn test_no_return_backward_compat() {
let source = r#"
class Foo
def bar
"hello"
end
end
"#;
let genv = setup_and_infer(source);
assert_eq!(get_return_type(&genv, "Foo", "bar"), "String");
}

#[test]
fn test_return_only_method() {
let source = r#"
class Foo
def bar
return "hello"
end
end
"#;
let genv = setup_and_infer(source);
assert_eq!(get_return_type(&genv, "Foo", "bar"), "String");
}

#[test]
fn test_return_dead_code_over_approximation() {
let source = r#"
class Foo
def bar
return "hello"
42
end
end
"#;
let genv = setup_and_infer(source);
let ty = get_return_type(&genv, "Foo", "bar");
// Dead code after return is still processed (over-approximation)
assert!(ty.contains("Integer"), "should contain Integer (dead code), got: {}", ty);
assert!(ty.contains("String"), "should contain String, got: {}", ty);
}
}
39 changes: 39 additions & 0 deletions test/loop_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,45 @@ def bar
assert_no_check_errors(source)
end

def test_while_modifier_form_no_error
source = <<~RUBY
class Foo
def bar
"hello".upcase while false
end
end
RUBY

assert_no_check_errors(source)
end

def test_begin_end_while_no_error
source = <<~RUBY
class Foo
def bar
begin
"hello".upcase
end while false
end
end
RUBY

assert_no_check_errors(source)
end

def test_for_empty_body_no_error
source = <<~RUBY
class Foo
def bar
for x in [1, 2, 3]
end
end
end
RUBY

assert_no_check_errors(source)
end

# ============================================
# Error Detection (check CLI)
# ============================================
Expand Down
Loading