diff --git a/core/src/analyzer/dispatch.rs b/core/src/analyzer/dispatch.rs index fa95d61..406443d 100644 --- a/core/src/analyzer/dispatch.rs +++ b/core/src/analyzer/dispatch.rs @@ -208,6 +208,16 @@ pub(crate) fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: & } } + // defined?(expr) → String | nil (child expression is NOT evaluated) + if node.as_defined_node().is_some() { + let result_vtx = genv.new_vertex(); + let str_vtx = genv.new_source(Type::string()); + let nil_vtx = genv.new_source(Type::Nil); + genv.add_edge(str_vtx, result_vtx); + genv.add_edge(nil_vtx, result_vtx); + return DispatchResult::Vertex(result_vtx); + } + DispatchResult::NotHandled } @@ -590,3 +600,68 @@ fn finish_method_call( ) -> VertexId { install_method_call(genv, recv_vtx, method_name, arg_vtxs, kwarg_vtxs, Some(location), safe_navigation) } + +#[cfg(test)] +mod tests { + use crate::analyzer::AstInstaller; + use crate::env::{GlobalEnv, LocalEnv}; + use crate::parser::ParseSession; + use crate::types::Type; + + fn parse_and_install(source: &str) -> GlobalEnv { + parse_and_install_with(source, |_| {}) + } + + fn parse_and_install_with_builtin(source: &str) -> GlobalEnv { + parse_and_install_with(source, |genv| { + genv.register_builtin_method(Type::string(), "upcase", Type::string()); + }) + } + + fn parse_and_install_with(source: &str, setup: impl FnOnce(&mut GlobalEnv)) -> GlobalEnv { + let session = ParseSession::new(); + let result = session.parse_source(source, "").unwrap(); + let mut genv = GlobalEnv::new(); + setup(&mut genv); + let mut lenv = LocalEnv::new(); + let mut installer = AstInstaller::new(&mut genv, &mut lenv, source); + + let root = 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 + } + + #[test] + fn test_defined_no_error() { + let genv = parse_and_install("result = defined?(foo)"); + assert!(genv.type_errors.is_empty()); + } + + #[test] + fn test_defined_result_is_string_or_nil() { + // Calling upcase on String | nil produces an error for the nil branch + let genv = parse_and_install_with_builtin( + "result = defined?(foo)\nresult.upcase", + ); + assert!( + !genv.type_errors.is_empty(), + "defined? returns String | nil, so upcase should error on nil branch" + ); + } + + #[test] + fn test_defined_child_not_evaluated() { + // If child expression were evaluated, 42.upcase would produce a type error + let genv = parse_and_install_with_builtin("defined?(42.upcase)"); + assert!( + genv.type_errors.is_empty(), + "Child expression of defined? should not be evaluated" + ); + } +} diff --git a/test/defined_test.rb b/test/defined_test.rb new file mode 100644 index 0000000..e73a021 --- /dev/null +++ b/test/defined_test.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'test_helper' + +class DefinedTest < Minitest::Test + include CLITestHelper + + # ============================================ + # No Error + # ============================================ + + def test_defined_basic + source = <<~RUBY + result = defined?(foo) + RUBY + assert_no_check_errors(source) + end + + def test_defined_with_method_call + source = <<~RUBY + defined?(some_method) + RUBY + assert_no_check_errors(source) + end + + def test_defined_child_not_evaluated + source = <<~RUBY + defined?(42.upcase) + RUBY + assert_no_check_errors(source) + end + + def test_defined_with_constant + source = <<~RUBY + defined?(CONSTANT) + RUBY + assert_no_check_errors(source) + end + + def test_defined_with_instance_variable + source = <<~RUBY + defined?(@var) + RUBY + assert_no_check_errors(source) + end + + # ============================================ + # Error Detection + # ============================================ + + def test_defined_result_string_method_error + source = <<~RUBY + class Foo + def bar + result = defined?(foo) + result.length + end + end + RUBY + assert_check_error(source, method_name: 'length', receiver_type: 'nil') + end + + def test_defined_result_integer_method_error + source = <<~RUBY + class Foo + def bar + result = defined?(foo) + result.even? + end + end + RUBY + assert_check_error(source, method_name: 'even?', receiver_type: 'String') + end +end