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
75 changes: 75 additions & 0 deletions core/src/analyzer/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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, "<test>").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"
);
}
}
74 changes: 74 additions & 0 deletions test/defined_test.rb
Original file line number Diff line number Diff line change
@@ -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