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
24 changes: 22 additions & 2 deletions crates/beamtalk-core/src/codegen/core_erlang/gen_server/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1824,21 +1824,41 @@ impl CoreErlangGenerator {
.collect();

// Build instance methods list (Primary methods only, using mangled selector names)
let methods: Vec<(String, usize)> = class
let mut methods: Vec<(String, usize)> = class
.methods
.iter()
.filter(|m| m.kind == MethodKind::Primary)
.map(|m| (m.selector.to_erlang_atom(), m.selector.arity()))
.collect();

// Build class methods list (Primary methods only)
let class_methods: Vec<(String, usize)> = class
let mut class_methods: Vec<(String, usize)> = class
.class_methods
.iter()
.filter(|m| m.kind == MethodKind::Primary)
.map(|m| (m.selector.to_erlang_atom(), m.selector.arity()))
.collect();

// BT-923: For `Value subclass:` classes, include auto-generated slot methods
// (getters, with*: setters, keyword constructor) so that reflection via
// `methods` / `allMethods` shows the full public interface.
if let Some(auto) =
crate::codegen::core_erlang::value_type_codegen::compute_auto_slot_methods(class)
{
use crate::codegen::core_erlang::value_type_codegen::AutoSlotMethods;
for field in &auto.getters {
methods.push((field.clone(), 0));
}
for field in &auto.setters {
let sel = AutoSlotMethods::with_star_selector(field);
methods.push((sel, 1));
}
if let Some(kw_sel) = &auto.keyword_constructor {
let arity = class.state.len();
class_methods.push((kw_sel.clone(), arity));
}
}

let fields_doc = Self::meta_atom_list(&fields);
let methods_doc = Self::meta_method_tuple_list(&methods);
let class_methods_doc = Self::meta_method_tuple_list(&class_methods);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ use crate::docvec;
///
/// Only populated when `class_kind == ClassKind::Value`.
/// Skips any slot whose getter/setter selector the user has already defined.
struct AutoSlotMethods {
pub(super) struct AutoSlotMethods {
/// Field names for which a getter `fieldName/1` is auto-generated.
getters: Vec<String>,
pub(super) getters: Vec<String>,
/// Field names for which a `withFieldName:/2` setter is auto-generated.
setters: Vec<String>,
pub(super) setters: Vec<String>,
/// Keyword constructor selector (e.g., `"x:y:"` for a Point with slots x, y),
/// `None` if the class has no slots or the user already defined it.
keyword_constructor: Option<String>,
pub(super) keyword_constructor: Option<String>,
}

impl AutoSlotMethods {
Expand All @@ -39,7 +39,7 @@ impl AutoSlotMethods {
/// Capitalises the first letter of the field name and prepends `"with"`:
/// - `"x"` → `"withX:"`
/// - `"firstName"` → `"withFirstName:"`
fn with_star_selector(field_name: &str) -> String {
pub(super) fn with_star_selector(field_name: &str) -> String {
let mut chars = field_name.chars();
match chars.next() {
None => "with:".to_string(),
Expand Down Expand Up @@ -67,7 +67,7 @@ impl AutoSlotMethods {
///
/// Returns `None` for `ClassKind::Object` and `ClassKind::Actor` — only
/// `ClassKind::Value` classes get auto-generated slot accessors.
fn compute_auto_slot_methods(class: &ClassDefinition) -> Option<AutoSlotMethods> {
pub(super) fn compute_auto_slot_methods(class: &ClassDefinition) -> Option<AutoSlotMethods> {
if class.class_kind != ClassKind::Value {
return None;
}
Expand Down
31 changes: 31 additions & 0 deletions crates/beamtalk-core/src/lint/effect_free_statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,37 @@ mod tests {
assert_eq!(effect_free[0].severity, Severity::Lint);
}

#[test]
fn discarded_map_literal_surfaced_by_lint_runner() {
let diags = lint("Object subclass: Foo\n bar =>\n #{#x => 1}\n self doSomething");
let effect_free: Vec<_> = diags
.iter()
.filter(|d| d.message.contains("no effect"))
.collect();
assert_eq!(
effect_free.len(),
1,
"Expected 1 effect-free lint for map literal, got: {effect_free:?}"
);
assert!(effect_free[0].message.contains("map literal"));
}

#[test]
fn map_literal_with_side_effect_not_flagged() {
// A map literal whose values contain side-effectful sends should NOT
// be flagged — the side effects execute even though the map is discarded.
let diags =
lint("Object subclass: Foo\n bar =>\n #{#x => self baz}\n self doSomething");
let effect_free: Vec<_> = diags
.iter()
.filter(|d| d.message.contains("map literal"))
.collect();
assert!(
effect_free.is_empty(),
"Map with side-effectful value should not be flagged: {effect_free:?}"
);
}

#[test]
fn clean_method_no_effect_free_lint() {
let diags = lint("Object subclass: Foo\n bar => 42");
Expand Down
Loading