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' }