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
32 changes: 32 additions & 0 deletions vlib/v/tests/same_name_types_str_methods/nostr/nostr.v
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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'
}
15 changes: 15 additions & 0 deletions vlib/v/tests/same_name_types_str_methods/toml_and_json2_test.v
Original file line number Diff line number Diff line change
@@ -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"}'
}
33 changes: 33 additions & 0 deletions vlib/v/tests/same_name_types_str_methods/withstr/withstr.v
Original file line number Diff line number Diff line change
@@ -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'
}
49 changes: 46 additions & 3 deletions vlib/v/transform/fn.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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..]
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions vlib/v/types/checker_tail.v
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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' {
Expand Down
72 changes: 67 additions & 5 deletions vlib/v/types/checker_tail_stmt.v
Original file line number Diff line number Diff line change
Expand Up @@ -10886,15 +10886,17 @@ 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
}
}
}
}
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
}
}
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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}`')
Expand Down Expand Up @@ -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
}
Expand All @@ -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'
}
Expand Down
Loading