From 3df0edaea04f5e7c95a51c018bb2c80e4e0e21cc Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 24 Sep 2026 15:02:26 +0300 Subject: [PATCH] v3: keep same-named types from using str methods of other modules Importing both `toml` and `x.json2` failed to compile: `toml.Any.string()` stringified its `[]Any` variant through `json2.[]Any.str`, emitting a copy of `[]toml.Any` into `[]json2.Any` that the C compiler rejected. Method lookup falls back to `receiver_method_suffix_index`, which is keyed by short receiver spellings (`[]Any.str`, `Any.str`). When only one module declares such a method, any same-named type from another module resolved to it. Reject suffix-index hits whose declaring module differs from the module of the receiver's struct, sum type or enum (arrays and maps use their element/value type), in the checker and the transformer. Aliases, interfaces and structs with embedded fields keep the old lookup, since they can inherit methods from other modules. The transformer also qualifies bare receiver types declared in the current module before asking the checker. --- .../same_name_types_str_methods/nostr/nostr.v | 32 +++++++++ .../same_name_types_str_methods_test.v | 61 ++++++++++++++++ .../toml_and_json2_test.v | 15 ++++ .../withstr/withstr.v | 33 +++++++++ vlib/v/transform/fn.v | 49 ++++++++++++- vlib/v/types/checker_tail.v | 4 +- vlib/v/types/checker_tail_stmt.v | 72 +++++++++++++++++-- 7 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 vlib/v/tests/same_name_types_str_methods/nostr/nostr.v create mode 100644 vlib/v/tests/same_name_types_str_methods/same_name_types_str_methods_test.v create mode 100644 vlib/v/tests/same_name_types_str_methods/toml_and_json2_test.v create mode 100644 vlib/v/tests/same_name_types_str_methods/withstr/withstr.v diff --git a/vlib/v/tests/same_name_types_str_methods/nostr/nostr.v b/vlib/v/tests/same_name_types_str_methods/nostr/nostr.v new file mode 100644 index 00000000000000..0fad10ddb75cd7 --- /dev/null +++ b/vlib/v/tests/same_name_types_str_methods/nostr/nostr.v @@ -0,0 +1,32 @@ +module nostr + +// Mirrors `toml.Any`: the same short type names as `withstr`, without `str` methods. +pub type Any = []Any | map[string]Any | int | string + +pub fn (a Any) string() string { + match a { + string { return a.clone() } + else { return a.str() } + } +} + +pub fn (a []Any) text() string { + return a.str() +} + +pub struct Point { +pub: + x int +} + +pub fn (p Point) text() string { + return p.str() +} + +pub enum Color { + red +} + +pub fn (c Color) text() string { + return c.str() +} diff --git a/vlib/v/tests/same_name_types_str_methods/same_name_types_str_methods_test.v b/vlib/v/tests/same_name_types_str_methods/same_name_types_str_methods_test.v new file mode 100644 index 00000000000000..842bbef7315565 --- /dev/null +++ b/vlib/v/tests/same_name_types_str_methods/same_name_types_str_methods_test.v @@ -0,0 +1,61 @@ +module main + +import nostr +import withstr + +// A `str` method declared for `withstr.Any`, `[]withstr.Any` or +// `map[string]withstr.Any` must not be used to stringify the same-named types +// from `nostr` (`json2.Any` vs `toml.Any`). +fn test_sum_type_str_does_not_use_same_named_type_methods() { + arr := nostr.Any([nostr.Any(1), nostr.Any('s')]) + for s in [arr.string(), arr.str(), '${arr}'] { + assert !s.contains('withstr'), s + assert s.starts_with('Any(['), s + } + m := nostr.Any({ + 'k': nostr.Any(2) + }) + for s in [m.string(), m.str(), '${m}'] { + assert !s.contains('withstr'), s + assert s.starts_with('Any({'), s + } + assert nostr.Any(3).string() == 'Any(3)' + assert nostr.Any('x').string() == 'x' + assert withstr.Any(1).str() == 'withstr-any' +} + +fn test_array_and_map_str_do_not_use_same_named_type_methods() { + arr := [nostr.Any(1), nostr.Any('s')] + for s in [arr.str(), arr.text(), '${arr}'] { + assert !s.contains('withstr'), s + assert s.contains('Any(1)'), s + assert s.contains("Any('s')"), s + } + m := { + 'k': nostr.Any(2) + } + for s in [m.str(), '${m}'] { + assert !s.contains('withstr'), s + assert s.contains('Any(2)'), s + } + assert [withstr.Any(1)].str() == 'withstr-array' + assert { + 'k': withstr.Any(1) + }.str() == 'withstr-map' +} + +fn test_struct_and_enum_str_do_not_use_same_named_type_methods() { + p := nostr.Point{ + x: 7 + } + for s in [p.str(), p.text(), '${p}'] { + assert !s.contains('withstr'), s + assert s.contains('x: 7'), s + } + c := nostr.Color.red + for s in [c.str(), c.text(), '${c}'] { + assert s == 'red', s + } + assert withstr.Point{}.str() == 'withstr-point' + assert withstr.Color.red.str() == 'withstr-color' +} diff --git a/vlib/v/tests/same_name_types_str_methods/toml_and_json2_test.v b/vlib/v/tests/same_name_types_str_methods/toml_and_json2_test.v new file mode 100644 index 00000000000000..b160004174bad0 --- /dev/null +++ b/vlib/v/tests/same_name_types_str_methods/toml_and_json2_test.v @@ -0,0 +1,15 @@ +import toml +import x.json2 + +struct TomlJson2Config { + key string +} + +// `toml.Any` has no `str` for its `[]Any` variant while `json2.Any` does; using +// both modules in one program must not route `toml.Any` through `json2.[]Any.str`. +fn test_toml_decode_and_json2_encode_in_one_program() { + t := toml.decode[TomlJson2Config]('key = "val"')! + assert t.key == 'val' + assert t.str().contains("key: 'val'") + assert json2.encode[TomlJson2Config](t) == '{"key":"val"}' +} diff --git a/vlib/v/tests/same_name_types_str_methods/withstr/withstr.v b/vlib/v/tests/same_name_types_str_methods/withstr/withstr.v new file mode 100644 index 00000000000000..92129eb0ccf11d --- /dev/null +++ b/vlib/v/tests/same_name_types_str_methods/withstr/withstr.v @@ -0,0 +1,33 @@ +module withstr + +// Mirrors `json2.Any`: `str` methods on the sum type, its array and its map. +pub type Any = []Any | map[string]Any | int | string + +pub fn (a []Any) str() string { + return 'withstr-array' +} + +pub fn (m map[string]Any) str() string { + return 'withstr-map' +} + +pub fn (a Any) str() string { + return 'withstr-any' +} + +pub struct Point { +pub: + x int +} + +pub fn (p Point) str() string { + return 'withstr-point' +} + +pub enum Color { + red +} + +pub fn (c Color) str() string { + return 'withstr-color' +} diff --git a/vlib/v/transform/fn.v b/vlib/v/transform/fn.v index 3e97f03d076934..7063f4335b90e0 100644 --- a/vlib/v/transform/fn.v +++ b/vlib/v/transform/fn.v @@ -407,7 +407,9 @@ fn (t &Transformer) resolve_receiver_method_for_type_uncached(receiver_type stri } } if !isnil(t.tc) { - if method_name := t.tc.concrete_method_signature_key(clean_type, method) { + if method_name := t.tc.concrete_method_signature_key(t.local_receiver_type_name(clean_type), + method) + { if t.is_known_fn_name(method_name) { return method_name } @@ -418,7 +420,13 @@ fn (t &Transformer) resolve_receiver_method_for_type_uncached(receiver_type stri return direct } if declared := t.declared_receiver_method(clean_type, method) { - return declared + // `declared` is the source spelling (`Any.str`); the suffix index names + // the module that registered it (`json2.Any.str`). + registered := t.receiver_method_suffix_index[declared] or { '' } + if registered == '' || registered == receiver_method_suffix_ambiguous + || t.receiver_owns_method(clean_type, registered) { + return declared + } } if clean_type.starts_with('main.') && !clean_type['main.'.len..].contains('.') { main_receiver := clean_type['main.'.len..] @@ -539,7 +547,9 @@ fn (t &Transformer) resolve_receiver_method_for_type_uncached(receiver_type stri } } if method_name := t.unique_receiver_method_suffix_match(t.receiver_method_candidates(clean_type, method)) { - return method_name + if t.receiver_owns_method(clean_type, method_name) { + return method_name + } } if !isnil(t.tc) { if target := t.alias_target_type_preserving_main_lock(clean_type) { @@ -553,6 +563,39 @@ fn (t &Transformer) resolve_receiver_method_for_type_uncached(receiver_type stri return none } +// local_receiver_type_name qualifies a bare receiver type, or the element type of +// a bare array receiver, that names a struct, sum type or enum declared in the +// current dependency module (`Any` -> `toml.Any`), so the type checker does not +// resolve it against a same-named type from another module. +fn (t &Transformer) local_receiver_type_name(clean_type string) string { + if isnil(t.tc) || !transform_can_prefix_collection_receiver(t.cur_module) { + return clean_type + } + mut elem := clean_type + for elem.starts_with('[]') { + elem = elem[2..] + } + if elem.len == 0 || elem.contains('.') || elem.contains('[') { + return clean_type + } + qualified := '${t.cur_module}.${elem}' + if qualified in t.tc.structs || qualified in t.tc.sum_types || qualified in t.tc.enum_names { + return clean_type[..clean_type.len - elem.len] + qualified + } + return clean_type +} + +// receiver_owns_method reports whether `method_name`, found by its short receiver +// spelling, is declared for `clean_type` rather than for a same-named type from +// another module (`json2.[]Any.str` must not stringify a `[]toml.Any`). +fn (t &Transformer) receiver_owns_method(clean_type string, method_name string) bool { + if isnil(t.tc) { + return true + } + receiver := t.tc.parse_type(t.local_receiver_type_name(clean_type)) + return t.tc.suffix_indexed_method_fits_receiver(receiver, method_name) +} + fn (t &Transformer) resolve_imported_flattened_generic_receiver_method(receiver_type string, method string) ?string { if receiver_type.contains('.') || !receiver_type.contains('_') || t.bare_struct_name_is_local_to_current_module(receiver_type) { diff --git a/vlib/v/types/checker_tail.v b/vlib/v/types/checker_tail.v index ab142f42852ba0..de8b9824ab8f27 100644 --- a/vlib/v/types/checker_tail.v +++ b/vlib/v/types/checker_tail.v @@ -5641,7 +5641,7 @@ fn (tc &TypeChecker) unknown_method_call_parts(node flat.Node) ?(flat.Node, Type return none } } - if _ := tc.unique_receiver_method_suffix_match(method_candidates) { + if _ := tc.unique_receiver_method_suffix_match(receiver_type, method_candidates) { return none } if receiver_type is Struct { @@ -8116,7 +8116,7 @@ fn (mut tc TypeChecker) resolve_call_info_uncached(id flat.NodeId, node flat.Nod return tc.call_info(mname, true) } } - if mname := tc.unique_receiver_method_suffix_match(array_candidates) { + if mname := tc.unique_receiver_method_suffix_match(clean_array, array_candidates) { return tc.call_info(mname, true) } if fn_node.value == 'get' { diff --git a/vlib/v/types/checker_tail_stmt.v b/vlib/v/types/checker_tail_stmt.v index 4aa4e67598eb29..7247714df36eb7 100644 --- a/vlib/v/types/checker_tail_stmt.v +++ b/vlib/v/types/checker_tail_stmt.v @@ -10886,7 +10886,8 @@ fn (tc &TypeChecker) concrete_method_signature_key_seen(concrete_name string, me return candidate } if indexed := tc.receiver_method_suffix_index[candidate] { - if indexed != receiver_method_suffix_ambiguous { + if indexed != receiver_method_suffix_ambiguous + && tc.suffix_indexed_method_fits_receiver(receiver_type.base_type, indexed) { return indexed } } @@ -10894,7 +10895,8 @@ fn (tc &TypeChecker) concrete_method_signature_key_seen(concrete_name string, me } for candidate in receiver_candidates { if indexed := tc.receiver_method_suffix_index[candidate] { - if indexed != receiver_method_suffix_ambiguous { + if indexed != receiver_method_suffix_ambiguous + && tc.suffix_indexed_method_fits_receiver(receiver_type, indexed) { return indexed } } @@ -10910,7 +10912,8 @@ fn (tc &TypeChecker) concrete_method_signature_key_seen(concrete_name string, me } } if indexed := tc.receiver_method_suffix_index[key] { - if indexed != receiver_method_suffix_ambiguous { + if indexed != receiver_method_suffix_ambiguous + && tc.suffix_indexed_method_fits_receiver(receiver_type, indexed) { return indexed } } @@ -16216,7 +16219,7 @@ fn (tc &TypeChecker) resolve_type_uncached(id flat.NodeId) Type { return tc.alias_return_type_from_text(mname) or { ret } } } - if mname := tc.unique_receiver_method_suffix_match(candidates) { + if mname := tc.unique_receiver_method_suffix_match(clean_type, candidates) { return tc.alias_return_type_from_text(mname) or { tc.fn_ret_types[mname] or { unknown_type('unknown return type for `${mname}`') @@ -18395,13 +18398,16 @@ fn push_receiver_method_candidate(mut names []string, name string) { } } -fn (tc &TypeChecker) unique_receiver_method_suffix_match(candidates []string) ?string { +fn (tc &TypeChecker) unique_receiver_method_suffix_match(receiver Type, candidates []string) ?string { mut found := '' for candidate in candidates { name := tc.receiver_method_suffix_index[candidate] or { continue } if name == receiver_method_suffix_ambiguous { return none } + if !tc.suffix_indexed_method_fits_receiver(receiver, name) { + continue + } if found != '' && found != name { return none } @@ -18413,6 +18419,62 @@ fn (tc &TypeChecker) unique_receiver_method_suffix_match(candidates []string) ?s return found } +// suffix_indexed_method_fits_receiver reports whether `indexed`, a method found +// through the short-name `receiver_method_suffix_index`, can belong to `receiver`. +// The index drops module prefixes, so a `[]toml.Any` receiver also reaches +// `json2.[]Any.str`; a method of a same-named type from another module must not +// bind to it. +pub fn (tc &TypeChecker) suffix_indexed_method_fits_receiver(receiver Type, indexed string) bool { + owner := tc.receiver_owner_module(receiver) or { return true } + return owner == type_owner_module(indexed.all_before_last('.')) +} + +// receiver_owner_module returns the module declaring the struct, sum type or enum +// whose methods `receiver` uses (the element or value type of arrays and maps). +// Aliases, interfaces and structs with embedded fields can inherit methods that +// are declared in other modules, so they report none. +fn (tc &TypeChecker) receiver_owner_module(receiver Type) ?string { + mut t := receiver + for { + if t is Pointer { + t = t.base_type + } else if t is Array { + t = t.elem_type + } else if t is ArrayFixed { + t = t.elem_type + } else if t is Map { + t = t.value_type + } else { + break + } + } + if t is Struct { + if tc.struct_fields_for_type(t.name).any(it.is_embed) { + return none + } + return type_owner_module(t.name) + } + if t is SumType { + return type_owner_module(t.name) + } + if t is Enum { + return type_owner_module(t.name) + } + return none +} + +// type_owner_module returns the module part of a type or method receiver name +// (`json2.Any`, `json2.[]Any`, `json2.Box[T]` -> `json2`). Main and builtin +// declarations are not module-qualified, so they all map to ''. +fn type_owner_module(name string) string { + head := name.all_before('[') + if !head.contains('.') { + return '' + } + module_name := head.all_before_last('.').all_after_last('.') + return if module_name in ['main', 'builtin'] { '' } else { module_name } +} + fn module_can_prefix_collection_receiver(module_name string) bool { return module_name != '' && module_name != 'main' && module_name != 'builtin' }