From d4c230633fa059793dddc241901156a2c0ec452c Mon Sep 17 00:00:00 2001 From: dak2 Date: Sat, 7 Feb 2026 15:54:55 +0900 Subject: [PATCH] Migrate Rust integration tests to Ruby CLI and Rust unit tests Rust integration tests tested the CLI pipeline through internal APIs, making them fragile to internal refactoring. Move error detection and location tests to Ruby CLI tests for true end-to-end validation, and keep type inference tests as Rust unit tests where they belong. --- rust/src/analyzer/install.rs | 235 ++++++ rust/src/analyzer/mod.rs | 3 - rust/src/analyzer/tests/integration_test.rs | 779 -------------------- rust/src/analyzer/tests/mod.rs | 1 - test/check_test.rb | 374 ++++++++++ test/test_helper.rb | 40 + 6 files changed, 649 insertions(+), 783 deletions(-) delete mode 100644 rust/src/analyzer/tests/integration_test.rs delete mode 100644 rust/src/analyzer/tests/mod.rs create mode 100644 test/check_test.rb diff --git a/rust/src/analyzer/install.rs b/rust/src/analyzer/install.rs index 5d480c0..5c7e9b2 100644 --- a/rust/src/analyzer/install.rs +++ b/rust/src/analyzer/install.rs @@ -539,6 +539,54 @@ mod tests { use crate::parser::ParseSession; use crate::types::Type; + /// Helper to run full analysis pipeline on Ruby source code + fn analyze(source: &str) -> (GlobalEnv, LocalEnv) { + let session = ParseSession::new(); + let parse_result = session.parse_source(source, "test.rb").unwrap(); + + let mut genv = GlobalEnv::new(); + + genv.register_builtin_method(Type::string(), "upcase", Type::string()); + genv.register_builtin_method(Type::string(), "downcase", Type::string()); + + genv.register_builtin_method(Type::float(), "to_s", Type::string()); + genv.register_builtin_method(Type::float(), "to_i", Type::integer()); + genv.register_builtin_method(Type::float(), "round", Type::integer()); + genv.register_builtin_method(Type::float(), "ceil", Type::integer()); + genv.register_builtin_method(Type::float(), "floor", Type::integer()); + genv.register_builtin_method(Type::float(), "abs", Type::float()); + + genv.register_builtin_method(Type::array(), "each", Type::array()); + genv.register_builtin_method(Type::array(), "map", Type::array()); + genv.register_builtin_method(Type::hash(), "each", Type::hash()); + + genv.register_builtin_method(Type::regexp(), "match", Type::instance("MatchData")); + genv.register_builtin_method(Type::regexp(), "match?", Type::instance("TrueClass")); + genv.register_builtin_method(Type::regexp(), "source", Type::string()); + + genv.register_builtin_method(Type::range(), "to_a", Type::array()); + genv.register_builtin_method(Type::range(), "size", Type::integer()); + genv.register_builtin_method(Type::range(), "count", Type::integer()); + genv.register_builtin_method(Type::range(), "first", Type::Bot); + genv.register_builtin_method(Type::range(), "last", Type::Bot); + genv.register_builtin_method(Type::range(), "include?", Type::instance("TrueClass")); + genv.register_builtin_method(Type::range(), "cover?", Type::instance("TrueClass")); + + let mut lenv = LocalEnv::new(); + let mut installer = AstInstaller::new(&mut genv, &mut lenv, source); + + let root = parse_result.node(); + if let Some(program_node) = root.as_program_node() { + let statements = program_node.statements(); + for stmt in &statements.body() { + installer.install_node(&stmt); + } + } + + installer.finish(); + (genv, lenv) + } + #[test] fn test_install_literal() { let source = r#"x = "hello""#; @@ -688,4 +736,191 @@ end assert_eq!(genv.scope_manager.current_module_name(), None); assert_eq!(genv.scope_manager.current_class_name(), None); } + + // ============================================ + // Float Type Inference Tests + // ============================================ + + #[test] + fn test_float_literal_basic() { + let (genv, lenv) = analyze(r#"x = 3.14"#); + let x_vtx = lenv.get_var("x").unwrap(); + assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Float"); + } + + #[test] + fn test_float_ceil() { + let (genv, lenv) = analyze("x = 3.14\na = x.ceil"); + let a_vtx = lenv.get_var("a").unwrap(); + assert_eq!(genv.get_vertex(a_vtx).unwrap().show(), "Integer"); + } + + #[test] + fn test_float_floor() { + let (genv, lenv) = analyze("x = 3.14\nb = x.floor"); + let b_vtx = lenv.get_var("b").unwrap(); + assert_eq!(genv.get_vertex(b_vtx).unwrap().show(), "Integer"); + } + + #[test] + fn test_float_abs() { + let (genv, lenv) = analyze("x = 3.14\nc = x.abs"); + let c_vtx = lenv.get_var("c").unwrap(); + assert_eq!(genv.get_vertex(c_vtx).unwrap().show(), "Float"); + } + + // ============================================ + // Regexp Type Inference Tests + // ============================================ + + #[test] + fn test_regexp_literal_basic() { + let (genv, lenv) = analyze(r#"x = /hello/"#); + let x_vtx = lenv.get_var("x").unwrap(); + assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Regexp"); + } + + #[test] + fn test_regexp_source() { + let (genv, lenv) = analyze("x = /hello/\na = x.source"); + let a_vtx = lenv.get_var("a").unwrap(); + assert_eq!(genv.get_vertex(a_vtx).unwrap().show(), "String"); + } + + // ============================================ + // Range Type Inference Tests + // ============================================ + + #[test] + fn test_range_integer() { + let (genv, lenv) = analyze(r#"x = 1..5"#); + let x_vtx = lenv.get_var("x").unwrap(); + assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range[Integer]"); + } + + #[test] + fn test_range_exclusive() { + let (genv, lenv) = analyze(r#"x = 1...5"#); + let x_vtx = lenv.get_var("x").unwrap(); + assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range[Integer]"); + } + + #[test] + fn test_range_string() { + let (genv, lenv) = analyze(r#"x = "a".."z""#); + let x_vtx = lenv.get_var("x").unwrap(); + assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range[String]"); + } + + #[test] + fn test_range_float() { + let (genv, lenv) = analyze(r#"x = 1.0..5.0"#); + let x_vtx = lenv.get_var("x").unwrap(); + assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range[Float]"); + } + + #[test] + fn test_range_to_a() { + let (genv, lenv) = analyze("x = 1..10\na = x.to_a"); + let a_vtx = lenv.get_var("a").unwrap(); + assert_eq!(genv.get_vertex(a_vtx).unwrap().show(), "Array"); + } + + #[test] + fn test_range_size() { + let (genv, lenv) = analyze("x = 1..10\nb = x.size"); + let b_vtx = lenv.get_var("b").unwrap(); + assert_eq!(genv.get_vertex(b_vtx).unwrap().show(), "Integer"); + } + + // ============================================ + // Nested Array Type Inference Tests + // ============================================ + + #[test] + fn test_nested_array_integer() { + let (genv, lenv) = analyze(r#"x = [[1, 2], [3]]"#); + let x_vtx = lenv.get_var("x").unwrap(); + assert_eq!( + genv.get_vertex(x_vtx).unwrap().show(), + "Array[Array[Integer]]" + ); + } + + #[test] + fn test_deeply_nested_array() { + let (genv, lenv) = analyze(r#"x = [[[1]]]"#); + let x_vtx = lenv.get_var("x").unwrap(); + assert_eq!( + genv.get_vertex(x_vtx).unwrap().show(), + "Array[Array[Array[Integer]]]" + ); + } + + #[test] + fn test_nested_array_mixed() { + let (genv, lenv) = analyze(r#"x = [[1], ["a"]]"#); + let x_vtx = lenv.get_var("x").unwrap(); + let result = genv.get_vertex(x_vtx).unwrap().show(); + assert!( + result == "Array[Array[Integer] | Array[String]]" + || result == "Array[Array[String] | Array[Integer]]", + "Expected nested array with union, got: {}", + result + ); + } + + // ============================================ + // Hash Type Inference Tests + // ============================================ + + #[test] + fn test_hash_symbol_integer() { + let (genv, lenv) = analyze(r#"x = { a: 1, b: 2 }"#); + let x_vtx = lenv.get_var("x").unwrap(); + assert_eq!( + genv.get_vertex(x_vtx).unwrap().show(), + "Hash[Symbol, Integer]" + ); + } + + #[test] + fn test_hash_string_string() { + let (genv, lenv) = analyze(r#"x = { "k" => "v" }"#); + let x_vtx = lenv.get_var("x").unwrap(); + assert_eq!( + genv.get_vertex(x_vtx).unwrap().show(), + "Hash[String, String]" + ); + } + + #[test] + fn test_hash_mixed_values() { + let (genv, lenv) = analyze(r#"x = { a: 1, b: "x" }"#); + let x_vtx = lenv.get_var("x").unwrap(); + let result = genv.get_vertex(x_vtx).unwrap().show(); + assert!( + result == "Hash[Symbol, Integer | String]" + || result == "Hash[Symbol, String | Integer]", + "Expected Hash with union value type, got: {}", + result + ); + } + + #[test] + fn test_hash_empty() { + let (genv, lenv) = analyze(r#"x = {}"#); + let x_vtx = lenv.get_var("x").unwrap(); + assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Hash"); + } + + #[test] + fn test_hash_nested() { + let (genv, lenv) = analyze(r#"x = { a: [1] }"#); + let x_vtx = lenv.get_var("x").unwrap(); + assert_eq!( + genv.get_vertex(x_vtx).unwrap().show(), + "Hash[Symbol, Array[Integer]]" + ); + } } diff --git a/rust/src/analyzer/mod.rs b/rust/src/analyzer/mod.rs index 9fff89d..3b15ffc 100644 --- a/rust/src/analyzer/mod.rs +++ b/rust/src/analyzer/mod.rs @@ -7,7 +7,4 @@ mod literals; mod parameters; mod variables; -#[cfg(test)] -mod tests; - pub use install::AstInstaller; diff --git a/rust/src/analyzer/tests/integration_test.rs b/rust/src/analyzer/tests/integration_test.rs deleted file mode 100644 index 4041a27..0000000 --- a/rust/src/analyzer/tests/integration_test.rs +++ /dev/null @@ -1,779 +0,0 @@ -//! Integration Tests - End-to-end analyzer tests -//! -//! This module contains integration tests that verify: -//! - Class/method definition handling -//! - Instance variable type tracking across methods -//! - Type error detection for undefined methods -//! - Method chain type inference - -use crate::analyzer::AstInstaller; -use crate::env::{GlobalEnv, LocalEnv}; -use crate::parser::ParseSession; -use crate::types::Type; - -/// Helper to run analysis on Ruby source code -fn analyze(source: &str) -> (GlobalEnv, LocalEnv) { - let session = ParseSession::new(); - let parse_result = session.parse_source(source, "test.rb").unwrap(); - - let mut genv = GlobalEnv::new(); - - // Register common methods - genv.register_builtin_method(Type::string(), "upcase", Type::string()); - genv.register_builtin_method(Type::string(), "downcase", Type::string()); - - // Register Float methods - genv.register_builtin_method(Type::float(), "to_s", Type::string()); - genv.register_builtin_method(Type::float(), "to_i", Type::integer()); - genv.register_builtin_method(Type::float(), "round", Type::integer()); - genv.register_builtin_method(Type::float(), "ceil", Type::integer()); - genv.register_builtin_method(Type::float(), "floor", Type::integer()); - genv.register_builtin_method(Type::float(), "abs", Type::float()); - - // Register iterator methods for block tests - genv.register_builtin_method(Type::array(), "each", Type::array()); - genv.register_builtin_method(Type::array(), "map", Type::array()); - genv.register_builtin_method(Type::hash(), "each", Type::hash()); - - // Register Regexp methods - genv.register_builtin_method(Type::regexp(), "match", Type::instance("MatchData")); - genv.register_builtin_method(Type::regexp(), "match?", Type::instance("TrueClass")); - genv.register_builtin_method(Type::regexp(), "source", Type::string()); - - // Register Range methods - genv.register_builtin_method(Type::range(), "to_a", Type::array()); - genv.register_builtin_method(Type::range(), "size", Type::integer()); - genv.register_builtin_method(Type::range(), "count", Type::integer()); - genv.register_builtin_method(Type::range(), "first", Type::Bot); - genv.register_builtin_method(Type::range(), "last", Type::Bot); - genv.register_builtin_method(Type::range(), "include?", Type::instance("TrueClass")); - genv.register_builtin_method(Type::range(), "cover?", Type::instance("TrueClass")); - - let mut lenv = LocalEnv::new(); - let mut installer = AstInstaller::new(&mut genv, &mut lenv, source); - - let root = parse_result.node(); - - if let Some(program_node) = root.as_program_node() { - let statements = program_node.statements(); - for stmt in &statements.body() { - installer.install_node(&stmt); - } - } - - installer.finish(); - - (genv, lenv) -} - -#[test] -fn test_class_method_error_detection() { - let source = r#" -class User - def test - x = 123 - y = x.upcase - end -end -"#; - - let (genv, _lenv) = analyze(source); - - // Type error should be detected: Integer doesn't have upcase method - assert_eq!(genv.type_errors.len(), 1); - assert_eq!(genv.type_errors[0].method_name, "upcase"); -} - -#[test] -fn test_class_with_instance_variable() { - let source = r#" -class User - def initialize - @name = "John" - end - - def greet - @name.upcase - end -end -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors should occur - @name is String - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_instance_variable_type_error() { - let source = r#" -class User - def initialize - @name = 123 - end - - def greet - @name.upcase - end -end -"#; - - let (genv, _lenv) = analyze(source); - - // Type error should be detected: @name is Integer, not String - assert_eq!(genv.type_errors.len(), 1); - assert_eq!(genv.type_errors[0].method_name, "upcase"); -} - -#[test] -fn test_multiple_classes() { - let source = r#" -class User - def name - x = 123 - x.upcase - end -end - -class Post - def title - y = "hello" - y.upcase - end -end -"#; - - let (genv, _lenv) = analyze(source); - - // Only User#name should have error (Integer#upcase), Post#title is fine - assert_eq!(genv.type_errors.len(), 1); - assert_eq!(genv.type_errors[0].method_name, "upcase"); -} - -#[test] -fn test_method_chain() { - let source = r#" -x = "hello" -y = x.upcase.downcase -"#; - - let (genv, lenv) = analyze(source); - - let y_vtx = lenv.get_var("y").unwrap(); - assert_eq!(genv.get_vertex(y_vtx).unwrap().show(), "String"); -} - -// ============================================ -// Method Parameter Tests -// ============================================ - -#[test] -fn test_method_parameter_available_as_local_var() { - let source = r#" -def greet(name) - x = name -end -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors should occur - name parameter should be available - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_method_multiple_parameters() { - let source = r#" -def calculate(a, b, c) - x = a - y = b - z = c -end -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors should occur - all parameters should be available - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_class_method_with_parameter() { - let source = r#" -class User - def initialize(name) - @name = name - end -end -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors should occur - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_parameter_method_call() { - // Parameter has Bot (untyped) type, so method calls won't error - // because we can't verify if the method exists on an untyped value - let source = r#" -def greet(name) - name.upcase -end -"#; - - let (genv, _lenv) = analyze(source); - - // With Bot type, we don't know if upcase exists or not - // Current behavior: Bot type means no method resolution, so no error - // This is acceptable for Phase 3 - we can improve later with call-site inference - assert!( - genv.type_errors.is_empty(), - "Bot (untyped) parameters should not produce method errors" - ); -} - -#[test] -fn test_optional_parameter_type_from_default() { - // Optional parameter with String default should have String type - let source = r#" -def greet(name = "World") - name.upcase -end -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors - name is String from default value, upcase exists on String - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_optional_parameter_type_error() { - // Optional parameter with Integer default should error on String method - let source = r#" -def greet(count = 42) - count.upcase -end -"#; - - let (genv, _lenv) = analyze(source); - - // Type error should be detected: count is Integer, upcase is not available - assert_eq!(genv.type_errors.len(), 1); - assert_eq!(genv.type_errors[0].method_name, "upcase"); -} - -#[test] -fn test_mixed_required_and_optional_parameters() { - let source = r#" -def greet(greeting, name = "World") - x = greeting - y = name.upcase -end -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors - name has String type from default - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_rest_parameter_has_array_type() { - let source = r#" -def collect(*items) - x = items -end -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors - items is Array - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_keyword_rest_parameter_has_hash_type() { - let source = r#" -def configure(**options) - x = options -end -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors - options is Hash - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_all_parameter_types_combined() { - let source = r#" -def complex_method(required, optional = "default", *rest, **kwargs) - a = required - b = optional.upcase - c = rest - d = kwargs -end -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors - optional.upcase should work (String has upcase) - assert_eq!(genv.type_errors.len(), 0); -} - -// ============================================ -// Block Tests -// ============================================ - -#[test] -fn test_block_parameter_available_as_local_var() { - let source = r#" -x = [1, 2, 3] -x.each { |item| y = item } -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors should occur - item parameter should be available in block - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_block_with_multiple_parameters() { - let source = r#" -x = { a: 1, b: 2 } -x.each { |key, value| a = key; b = value } -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors should occur - both parameters should be available - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_block_do_end_syntax() { - let source = r#" -x = [1, 2, 3] -x.map do |item| - y = item -end -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors should occur - do...end blocks work the same as { } - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_block_accesses_outer_scope_variable() { - let source = r#" -outer = "hello" -x = [1, 2, 3] -x.each { |item| y = outer.upcase } -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors - block can access outer scope variable - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_nested_blocks() { - let source = r#" -x = [[1, 2], [3, 4]] -x.each { |row| row.each { |item| y = item } } -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors should occur - nested blocks work correctly - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_block_in_method_definition() { - let source = r#" -def process_items - items = [1, 2, 3] - items.each { |item| x = item } -end -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors should occur - blocks work inside methods - assert_eq!(genv.type_errors.len(), 0); -} - -#[test] -fn test_block_in_class_method() { - let source = r#" -class Processor - def process - items = [1, 2, 3] - items.map { |item| item } - end -end -"#; - - let (genv, _lenv) = analyze(source); - - // No type errors should occur - assert_eq!(genv.type_errors.len(), 0); -} - -// ============================================ -// Float Literal Tests -// ============================================ - -#[test] -fn test_float_literal_basic() { - let source = r#"x = 3.14"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Float"); -} - -#[test] -fn test_float_literal_type_error() { - let source = r#" -class Calculator - def compute - x = 3.14 - y = x.upcase - end -end -"#; - - let (genv, _lenv) = analyze(source); - - // Type error should be detected: Float doesn't have upcase method - assert_eq!(genv.type_errors.len(), 1); - assert_eq!(genv.type_errors[0].method_name, "upcase"); -} - -#[test] -fn test_float_specific_methods() { - let source = r#" -x = 3.14 -a = x.ceil -b = x.floor -c = x.abs -"#; - - let (genv, lenv) = analyze(source); - - // No type errors - ceil, floor, abs are valid Float methods - assert_eq!(genv.type_errors.len(), 0); - - // ceil and floor return Integer - let a_vtx = lenv.get_var("a").unwrap(); - assert_eq!(genv.get_vertex(a_vtx).unwrap().show(), "Integer"); - - let b_vtx = lenv.get_var("b").unwrap(); - assert_eq!(genv.get_vertex(b_vtx).unwrap().show(), "Integer"); - - // abs returns Float - let c_vtx = lenv.get_var("c").unwrap(); - assert_eq!(genv.get_vertex(c_vtx).unwrap().show(), "Float"); -} - -// ============================================ -// Regexp Literal Tests -// ============================================ - -#[test] -fn test_regexp_literal_basic() { - let source = r#"x = /hello/"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Regexp"); -} - -#[test] -fn test_regexp_literal_type_error() { - let source = r#" -class Matcher - def find - x = /pattern/ - y = x.upcase - end -end -"#; - - let (genv, _lenv) = analyze(source); - - // Type error should be detected: Regexp doesn't have upcase method - assert_eq!(genv.type_errors.len(), 1); - assert_eq!(genv.type_errors[0].method_name, "upcase"); -} - -#[test] -fn test_regexp_specific_methods() { - let source = r#" -x = /hello/ -a = x.source -"#; - - let (genv, lenv) = analyze(source); - - // No type errors - source is a valid Regexp method - assert_eq!(genv.type_errors.len(), 0); - - // source returns String - let a_vtx = lenv.get_var("a").unwrap(); - assert_eq!(genv.get_vertex(a_vtx).unwrap().show(), "String"); -} - -// ============================================ -// Range Literal Tests -// ============================================ - -#[test] -fn test_range_literal_basic() { - let source = r#"x = 1..5"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range[Integer]"); -} - -#[test] -fn test_range_literal_exclusive() { - let source = r#"x = 1...5"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range[Integer]"); -} - -#[test] -fn test_range_literal_string() { - let source = r#"x = "a".."z""#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range[String]"); -} - -#[test] -fn test_range_generic_float() { - let source = r#"x = 1.0..5.0"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range[Float]"); -} - -// ============================================ -// Nested Generic Array Tests -// ============================================ - -#[test] -fn test_nested_array_integer() { - let source = r#"x = [[1, 2], [3]]"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - assert_eq!( - genv.get_vertex(x_vtx).unwrap().show(), - "Array[Array[Integer]]" - ); -} - -#[test] -fn test_deeply_nested_array() { - let source = r#"x = [[[1]]]"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - assert_eq!( - genv.get_vertex(x_vtx).unwrap().show(), - "Array[Array[Array[Integer]]]" - ); -} - -#[test] -fn test_nested_array_mixed() { - let source = r#"x = [[1], ["a"]]"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - // Union type order may vary, so check for either order - let result = genv.get_vertex(x_vtx).unwrap().show(); - assert!( - result == "Array[Array[Integer] | Array[String]]" - || result == "Array[Array[String] | Array[Integer]]", - "Expected nested array with union, got: {}", - result - ); -} - -#[test] -fn test_range_literal_type_error() { - let source = r#" -class Calculator - def compute - x = 1..10 - y = x.upcase - end -end -"#; - - let (genv, _lenv) = analyze(source); - - assert_eq!(genv.type_errors.len(), 1); - assert_eq!(genv.type_errors[0].method_name, "upcase"); -} - -#[test] -fn test_range_specific_methods() { - let source = r#" -x = 1..10 -a = x.to_a -b = x.size -"#; - - let (genv, lenv) = analyze(source); - - assert_eq!(genv.type_errors.len(), 0); - - let a_vtx = lenv.get_var("a").unwrap(); - assert_eq!(genv.get_vertex(a_vtx).unwrap().show(), "Array"); - - let b_vtx = lenv.get_var("b").unwrap(); - assert_eq!(genv.get_vertex(b_vtx).unwrap().show(), "Integer"); -} - -// ============================================ -// Hash Generic Type Tests -// ============================================ - -#[test] -fn test_hash_symbol_integer() { - let source = r#"x = { a: 1, b: 2 }"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - assert_eq!( - genv.get_vertex(x_vtx).unwrap().show(), - "Hash[Symbol, Integer]" - ); -} - -#[test] -fn test_hash_string_string() { - let source = r#"x = { "k" => "v" }"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - assert_eq!( - genv.get_vertex(x_vtx).unwrap().show(), - "Hash[String, String]" - ); -} - -#[test] -fn test_hash_mixed_values() { - let source = r#"x = { a: 1, b: "x" }"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - let result = genv.get_vertex(x_vtx).unwrap().show(); - // Union type order may vary - assert!( - result == "Hash[Symbol, Integer | String]" - || result == "Hash[Symbol, String | Integer]", - "Expected Hash with union value type, got: {}", - result - ); -} - -#[test] -fn test_hash_empty() { - let source = r#"x = {}"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Hash"); -} - -#[test] -fn test_hash_nested() { - let source = r#"x = { a: [1] }"#; - - let (genv, lenv) = analyze(source); - - let x_vtx = lenv.get_var("x").unwrap(); - assert_eq!( - genv.get_vertex(x_vtx).unwrap().show(), - "Hash[Symbol, Array[Integer]]" - ); -} - -// ============================================ -// Error Location Tests -// ============================================ - -#[test] -fn test_error_location_points_to_dot() { - // Test that error location points to the dot operator, not the receiver - // "name.abs" - error should point to column 5 (the `.`) - let source = "name = \"x\"\nname.abs"; - - let (genv, _lenv) = analyze(source); - - assert_eq!(genv.type_errors.len(), 1); - let error = &genv.type_errors[0]; - - // Location should be present - assert!(error.location.is_some()); - let loc = error.location.as_ref().unwrap(); - - // Line 2, column 5 (1-indexed) - the dot position - assert_eq!(loc.line, 2); - assert_eq!(loc.column, 5); // "name" is 4 chars, "." is at column 5 -} - -#[test] -fn test_error_location_method_chain() { - // Test error location in method chain: "x.upcase.foo" - // Error should point to the second dot (for undefined method `foo`) - let source = "x = \"hello\"\ny = x.upcase.foo"; - - let (genv, _lenv) = analyze(source); - - assert_eq!(genv.type_errors.len(), 1); - let error = &genv.type_errors[0]; - assert_eq!(error.method_name, "foo"); - - assert!(error.location.is_some()); - let loc = error.location.as_ref().unwrap(); - - // Line 2, the second dot position - assert_eq!(loc.line, 2); - // "y = x.upcase" is 12 chars, "." is at column 13 - assert_eq!(loc.column, 13); -} diff --git a/rust/src/analyzer/tests/mod.rs b/rust/src/analyzer/tests/mod.rs deleted file mode 100644 index 2758dbf..0000000 --- a/rust/src/analyzer/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod integration_test; diff --git a/test/check_test.rb b/test/check_test.rb new file mode 100644 index 0000000..d440c30 --- /dev/null +++ b/test/check_test.rb @@ -0,0 +1,374 @@ +# frozen_string_literal: true + +require 'test_helper' + +class CheckTest < Minitest::Test + include CLITestHelper + + # ============================================ + # Error Detection Tests (7) + # ============================================ + + def test_class_method_error_detection + source = <<~RUBY + class User + def test + x = 123 + y = x.upcase + end + end + RUBY + + assert_check_error(source, method_name: 'upcase', receiver_type: 'Integer') + end + + def test_instance_variable_type_error + source = <<~RUBY + class User + def initialize + @name = 123 + end + + def greet + @name.upcase + end + end + RUBY + + assert_check_error(source, method_name: 'upcase', receiver_type: 'Integer') + end + + def test_multiple_classes + source = <<~RUBY + class User + def name + x = 123 + x.upcase + end + end + + class Post + def title + y = "hello" + y.upcase + end + end + RUBY + + stdout, _stderr, status = run_check(source) + + refute status.success? + assert_match(/undefined method `upcase` for Integer/, stdout) + refute_match(/Post/, stdout) + end + + def test_float_literal_type_error + source = <<~RUBY + class Calculator + def compute + x = 3.14 + y = x.upcase + end + end + RUBY + + assert_check_error(source, method_name: 'upcase', receiver_type: 'Float') + end + + def test_regexp_literal_type_error + source = <<~RUBY + class Matcher + def find + x = /pattern/ + y = x.upcase + end + end + RUBY + + assert_check_error(source, method_name: 'upcase', receiver_type: 'Regexp') + end + + def test_optional_parameter_type_error + source = <<~RUBY + def greet(count = 42) + count.upcase + end + RUBY + + assert_check_error(source, method_name: 'upcase', receiver_type: 'Integer') + end + + def test_range_literal_type_error + source = <<~RUBY + class Calculator + def compute + x = 1..10 + y = x.upcase + end + end + RUBY + + assert_check_error(source, method_name: 'upcase', receiver_type: 'Range') + end + + # ============================================ + # No Error Tests - Instance Variables (1) + # ============================================ + + def test_class_with_instance_variable + source = <<~RUBY + class User + def initialize + @name = "John" + end + + def greet + @name.upcase + end + end + RUBY + + assert_no_check_errors(source) + end + + # ============================================ + # No Error Tests - Method Parameters (8) + # ============================================ + + def test_method_parameter_available_as_local_var + source = <<~RUBY + def greet(name) + x = name + end + RUBY + + assert_no_check_errors(source) + end + + def test_method_multiple_parameters + source = <<~RUBY + def calculate(a, b, c) + x = a + y = b + z = c + end + RUBY + + assert_no_check_errors(source) + end + + def test_class_method_with_parameter + source = <<~RUBY + class User + def initialize(name) + @name = name + end + end + RUBY + + assert_no_check_errors(source) + end + + def test_parameter_method_call_bot_type + source = <<~RUBY + def greet(name) + name.upcase + end + RUBY + + assert_no_check_errors(source) + end + + def test_optional_parameter_type_from_default + source = <<~RUBY + def greet(name = "World") + name.upcase + end + RUBY + + assert_no_check_errors(source) + end + + def test_mixed_required_and_optional_parameters + source = <<~RUBY + def greet(greeting, name = "World") + x = greeting + y = name.upcase + end + RUBY + + assert_no_check_errors(source) + end + + def test_rest_parameter_has_array_type + source = <<~RUBY + def collect(*items) + x = items + end + RUBY + + assert_no_check_errors(source) + end + + def test_keyword_rest_parameter_has_hash_type + source = <<~RUBY + def configure(**options) + x = options + end + RUBY + + assert_no_check_errors(source) + end + + # ============================================ + # No Error Tests - Blocks (7) + # ============================================ + + def test_block_parameter_available_as_local_var + source = <<~RUBY + x = [1, 2, 3] + x.each { |item| y = item } + RUBY + + assert_no_check_errors(source) + end + + def test_block_with_multiple_parameters + source = <<~RUBY + x = { a: 1, b: 2 } + x.each { |key, value| a = key; b = value } + RUBY + + assert_no_check_errors(source) + end + + def test_block_do_end_syntax + source = <<~RUBY + x = [1, 2, 3] + x.map do |item| + y = item + end + RUBY + + assert_no_check_errors(source) + end + + def test_block_accesses_outer_scope_variable + source = <<~RUBY + outer = "hello" + x = [1, 2, 3] + x.each { |item| y = outer.upcase } + RUBY + + assert_no_check_errors(source) + end + + def test_nested_blocks + source = <<~RUBY + x = [[1, 2], [3, 4]] + x.each { |row| row.each { |item| y = item } } + RUBY + + assert_no_check_errors(source) + end + + def test_block_in_method_definition + source = <<~RUBY + def process_items + items = [1, 2, 3] + items.each { |item| x = item } + end + RUBY + + assert_no_check_errors(source) + end + + def test_block_in_class_method + source = <<~RUBY + class Processor + def process + items = [1, 2, 3] + items.map { |item| item } + end + end + RUBY + + assert_no_check_errors(source) + end + + # ============================================ + # No Error Tests - Method Calls (3) + # ============================================ + + def test_float_methods_no_error + source = <<~RUBY + x = 3.14 + a = x.ceil + b = x.floor + c = x.abs + RUBY + + assert_no_check_errors(source) + end + + def test_regexp_methods_no_error + source = <<~RUBY + x = /hello/ + a = x.source + RUBY + + assert_no_check_errors(source) + end + + def test_range_methods_no_error + source = <<~RUBY + x = 1..10 + a = x.to_a + b = x.size + RUBY + + assert_no_check_errors(source) + end + + # ============================================ + # No Error Tests - Method Chains (2) + # ============================================ + + def test_method_chain_no_error + source = <<~RUBY + x = "hello" + y = x.upcase.downcase + RUBY + + assert_no_check_errors(source) + end + + def test_all_parameter_types_combined + source = <<~RUBY + def complex_method(required, optional = "default", *rest, **kwargs) + a = required + b = optional.upcase + c = rest + d = kwargs + end + RUBY + + assert_no_check_errors(source) + end + + # ============================================ + # Error Location Tests (2) + # ============================================ + + def test_error_location_points_to_dot + source = "name = \"x\"\nname.abs" + + assert_error_at(source, line: 2, column: 5) + end + + def test_error_location_method_chain + source = "x = \"hello\"\ny = x.upcase.foo" + + assert_error_at(source, line: 2, column: 13) + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 2976e05..6cdf7a1 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,4 +1,44 @@ $LOAD_PATH.unshift File.expand_path("../lib", __dir__) require "methodray" +# Ensure RBS cache exists before any test runs (CheckTest CLI tests depend on it) +MethodRay.setup + require "minitest/autorun" +require "tempfile" +require "open3" + +module CLITestHelper + private + + def run_check(source) + file = Tempfile.new(['check_test', '.rb']) + file.write(source) + file.close + + stdout, stderr, status = Open3.capture3('bundle', 'exec', 'methodray', 'check', file.path) + [stdout, stderr, status] + ensure + file&.unlink + end + + def assert_check_error(source, method_name:, receiver_type:) + stdout, _stderr, status = run_check(source) + + refute status.success?, "Expected check to fail but it succeeded" + assert_match(/undefined method `#{Regexp.escape(method_name)}` for #{Regexp.escape(receiver_type)}/, stdout) + end + + def assert_no_check_errors(source) + stdout, _stderr, status = run_check(source) + + assert status.success?, "Expected check to pass but it failed.\nOutput: #{stdout}" + end + + def assert_error_at(source, line:, column:) + stdout, _stderr, status = run_check(source) + + refute status.success?, "Expected check to fail but it succeeded" + assert_match(/:#{line}:#{column}:/, stdout, "Expected error at line #{line}, column #{column}") + end +end