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
13 changes: 9 additions & 4 deletions core/src/analyzer/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,17 @@ pub(crate) fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &
}

// Class variable read: @@name
// Ruby: uninitialized class variables raise NameError at runtime.
// We fall back to nil so downstream method calls are still type-checked
// rather than silently skipped. This may produce nil-union types when
// reads precede writes in source order.
if let Some(cvar_read) = node.as_class_variable_read_node() {
let cvar_name = bytes_to_name(cvar_read.name().as_slice());
return match install_class_var_read(genv, &cvar_name) {
Some(vtx) => DispatchResult::Vertex(vtx),
None => DispatchResult::NotHandled,
};
let vtx = install_class_var_read(genv, &cvar_name).unwrap_or_else(|| {
let nil_vtx = genv.new_source(Type::Nil);
install_class_var_write(genv, cvar_name, nil_vtx)
});
return DispatchResult::Vertex(vtx);
}

// Global variable read: $name
Expand Down
7 changes: 3 additions & 4 deletions core/src/env/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,10 +326,9 @@ impl ScopeManager {
.find(|scope| matches!(&scope.kind, ScopeKind::Class { .. }))
.map(|scope| scope.id);
if let Some(scope_id) = class_scope_id {
self.scopes
.get_mut(&scope_id)
.expect("scope id from walk_scopes must exist")
.set_class_var(name, vtx);
if let Some(scope) = self.scopes.get_mut(&scope_id) {
scope.set_class_var(name, vtx);
}
}
}
}
Expand Down
57 changes: 53 additions & 4 deletions test/class_variable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ def log(msg)
assert_no_check_errors(source)
end

# ============================================
# Error Detection
# ============================================

def test_class_variable_write_before_read
source = <<~RUBY
class Config
Expand All @@ -60,6 +56,10 @@ def env
assert_no_check_errors(source)
end

# ============================================
# Error Detection
# ============================================

def test_class_variable_type_error
source = <<~RUBY
class Counter
Expand All @@ -86,4 +86,53 @@ def run
RUBY
assert_check_error(source, method_name: 'length', receiver_type: 'Integer')
end

def test_class_variable_class_body_write_type_error
source = <<~RUBY
class Counter
@@count = 42

def display
@@count.upcase
end
end
RUBY
assert_check_error(source, method_name: 'upcase', receiver_type: 'Integer')
end

def test_class_variable_isolation_between_classes
source = <<~RUBY
class StringHolder
def setup
@@value = "hello"
end

def run
@@value.upcase
end
end

class IntHolder
def setup
@@value = 42
end

def run
@@value.upcase
end
end
RUBY
assert_check_error(source, method_name: 'upcase', receiver_type: 'Integer')
end

def test_class_variable_in_module_no_crash
source = <<~RUBY
module Config
@@setting = "production"
end
RUBY
# Module-scoped @@var is not yet supported (v0.2.0 limitation).
# The checker should not crash.
assert_no_check_errors(source)
end
end