From 6e6ec15b73fd3b9022c68d1f4a0d42afb3b4d5d0 Mon Sep 17 00:00:00 2001 From: MARCROCK22 Date: Fri, 2 Oct 2026 18:03:07 -0400 Subject: [PATCH] types: let a method's receiver belong to it in editor queries The node of a method is at its name, after its receiver, so the editor's lookup of a type parameter written in a signature (vls_decl_at) never reached the method when the cursor was on the `T` of `fn (b Box[T]) get() T`. It took the declaration before instead, and since it checks that the cursor is inside it, hover and go-to-definition there answered nothing. Before that check, they answered with whatever came before: the `T` of the struct, of the receiver of the previous method, or of an unrelated generic function. vls_decl_start makes a method start at its receiver, the `(` that matches the `)` right before its name, where vls_type_param_declared_at already finds the type parameters of the method. Functions, static methods and C functions still start at their name, and declarations that share a line of a `.vh` header keep their own starts. --- .../vls_generic_parameter_scope_review_test.v | 90 +++++++++++++++++++ vlib/v/types/vls_header_scope_review_test.v | 63 +++++++++++++ vlib/v/types/vls_query.v | 36 +++++++- 3 files changed, 187 insertions(+), 2 deletions(-) diff --git a/vlib/v/compiler_tests/vls_generic_parameter_scope_review_test.v b/vlib/v/compiler_tests/vls_generic_parameter_scope_review_test.v index ae8295fccd7168..9b9fe800e36d6f 100644 --- a/vlib/v/compiler_tests/vls_generic_parameter_scope_review_test.v +++ b/vlib/v/compiler_tests/vls_generic_parameter_scope_review_test.v @@ -34,3 +34,93 @@ fn test_type_parameter_hover_and_definition_stay_in_their_declaration_scope() { assert scope_review_query(path, '19:19', 'hv').contains('[T]') assert scope_review_query(path, '19:19', 'gd') == '${path}:19:12' } + +// receiver_scope_layouts put right before a method of `Box[T]` each kind of +// declaration that the `T` written in its receiver was taken from: its struct, +// another method, and a generic function with and without a `T` of its own. +const receiver_scope_layouts = [ + '', + 'fn (b Box[T]) first() T {\n\treturn b.value\n}\n\n', + 'fn identity[T](x T) T {\n\treturn x\n}\n\n', + 'fn helper[U](u U) U {\n\treturn u\n}\n\n', +] + +const receiver_forms_program = 'module main\n\nstruct T {\n\tvalue int\n}\n\nstruct Box[T] {\n\tvalue T\n}\n\nstruct Pair[K, V] {\n\tkey K\n\tvalue V\n}\n\nfn (p Pair[K, V]) entry() (K, V) {\n\treturn p.key, p.value\n}\n\nfn (mut b Box[T]) set(value T) {\n\tb.value = value\n}\n\nfn (b &Box[T]) peek() T {\n\treturn b.value\n}\n\nfn (b Box[T]) convert[U](f fn (T) U) Box[U] {\n\treturn Box[U]{\n\t\tvalue: f(b.value)\n\t}\n}\n\n__global after_methods T = T{}\n\nfn main() {\n\tprintln(after_methods.value)\n}\n' + +// word_position is `line:column` of the `nth` type parameter `name` in the +// first line of `source` that starts with `prefix`. +fn word_position(source string, prefix string, name string, nth int) string { + mut seen := 0 + for i, line in source.split_into_lines() { + if !line.starts_with(prefix) { + continue + } + for col, c in line { + if c == name[0] && (col == 0 || !line[col - 1].is_alnum()) + && (col + 1 == line.len || !line[col + 1].is_alnum()) { + if seen == nth { + return '${i + 1}:${col}' + } + seen++ + } + } + break + } + panic('no `${name}` #${nth} in the line `${prefix}`') +} + +fn test_a_type_parameter_in_a_method_receiver_belongs_to_the_method() { + root := os.join_path(os.vtmp_dir(), 'vls_receiver_scope_review_${os.getpid()}') + os.mkdir_all(root)! + defer { os.rmdir_all(root) or {} } + for i, layout in receiver_scope_layouts { + path := os.join_path(root, 'layout_${i}.v') + source := 'module main\n\nstruct Box[T] {\n\tvalue T\n}\n\n${layout}fn (b Box[T]) get() T {\n\t_ := []T{}\n\treturn b.value\n}\n\nfn main() {\n\tprintln(Box[int]{}.get())\n}\n' + os.write_file(path, source)! + receiver := word_position(source, 'fn (b Box[T]) get', 'T', 0) + for position in [receiver, word_position(source, 'fn (b Box[T]) get', 'T', 1), + word_position(source, '\t_ := []T{}', 'T', 0)] { + assert scope_review_query(path, position, 'hv').contains('[T]'), 'layout ${i} at ${position}' + assert scope_review_query(path, position, 'gd') == '${path}:${receiver}', 'layout ${i} at ${position}' + } + if layout.starts_with('fn (b Box[T]) first') { + first := word_position(source, 'fn (b Box[T]) first', 'T', 0) + assert scope_review_query(path, first, 'gd') == '${path}:${first}' + } + } +} + +fn test_type_parameters_in_receivers_of_every_form_belong_to_their_method() { + root := os.join_path(os.vtmp_dir(), 'vls_receiver_forms_review_${os.getpid()}') + os.mkdir_all(root)! + defer { os.rmdir_all(root) or {} } + path := os.join_path(root, 'forms.v') + source := receiver_forms_program + os.write_file(path, source)! + for prefix, names in { + 'fn (p Pair[K, V]) entry': ['K', 'V'] + 'fn (mut b Box[T]) set': ['T'] + 'fn (b &Box[T]) peek': ['T'] + 'fn (b Box[T]) convert': ['T'] + } { + for name in names { + declared := word_position(source, prefix, name, 0) + hover := scope_review_query(path, declared, 'hv') + assert hover.contains('[${name}]'), '${prefix}: ${hover}' + assert !hover.contains('struct'), '${prefix}: ${hover}' + assert scope_review_query(path, declared, 'gd') == '${path}:${declared}', prefix + // Its next use in the signature leads to the receiver too. + used := word_position(source, prefix, name, 1) + assert scope_review_query(path, used, 'gd') == '${path}:${declared}', '${prefix} ${used}' + } + } + // The own type parameter of the method is declared after its name. + own := word_position(source, 'fn (b Box[T]) convert', 'U', 0) + result := word_position(source, 'fn (b Box[T]) convert', 'U', 2) + assert scope_review_query(path, result, 'gd') == '${path}:${own}' + // A declaration after the methods still finds the module type `T`. + after := word_position(source, '__global after_methods', 'T', 0) + hover := scope_review_query(path, after, 'hv') + assert hover.contains('struct T') && !hover.contains('[T]'), hover + assert scope_review_query(path, after, 'gd') == '${path}:3:7' +} diff --git a/vlib/v/types/vls_header_scope_review_test.v b/vlib/v/types/vls_header_scope_review_test.v index d1c2e8d0e6f3e3..f38fe3ab911fc8 100644 --- a/vlib/v/types/vls_header_scope_review_test.v +++ b/vlib/v/types/vls_header_scope_review_test.v @@ -55,3 +55,66 @@ fn test_bodyless_generic_header_keeps_function_return_types_in_scope() { } } } + +// The node of a method is at its name: the receiver before it declares type +// parameters of the method too, so it belongs to the declaration. +fn test_a_method_declaration_starts_at_its_receiver() { + for source, name in { + 'fn (b Box[T]) get() T {\n\treturn b.value\n}\n': 'get' + 'pub fn (mut b Box[T]) set(value T) {\n}\n': 'set' + 'fn (b Box[fn (T) T]) call(x T) T {\n\treturn x\n}\n': 'call' + 'fn (a Vec[T]) + (b Vec[T]) Vec[T] {\n\treturn a\n}\n': '+' + } { + mut a := flat.FlatAst.new() + mut file_set := token.FileSet.new() + a.source_files[1] = file_set.add_file('receiver.v', source.len) + name_at := (source.index(') ${name}') or { panic('missing ${name}') }) + 2 + decl := a.add_node(flat.Node{ + kind: .fn_decl + pos: token.new_pos(1, name_at) + }) + mut tc := TypeChecker.new(&a) + tc.source_texts_by_file['receiver.v'] = source + receiver := source.index('T') or { panic('missing receiver T') } + found := tc.vls_decl_at(1, receiver) or { flat.NodeId(-1) } + assert found == decl, source + } + // A static method has no receiver: what comes before its name is not its. + source := 'fn Box.new[T](value T) Box[T] {\n\treturn Box[T]{}\n}\n' + mut a := flat.FlatAst.new() + mut file_set := token.FileSet.new() + a.source_files[1] = file_set.add_file('static.v', source.len) + a.add_node(flat.Node{ + kind: .fn_decl + pos: token.new_pos(1, source.index('new') or { panic('missing new') }) + }) + mut tc := TypeChecker.new(&a) + tc.source_texts_by_file['static.v'] = source + found := tc.vls_decl_at(1, source.index('Box') or { panic('missing Box') }) or { + flat.NodeId(-1) + } + assert found == flat.NodeId(-1) +} + +fn test_a_header_method_on_the_line_of_another_declaration_keeps_its_receiver() { + source := 'fn first[U](x U) U; fn (b Box[T]) get() T\n' + mut a := flat.FlatAst.new() + mut file_set := token.FileSet.new() + a.source_files[1] = file_set.add_file('same_line.vh', source.len) + first := a.add_node(flat.Node{ + kind: .fn_decl + pos: token.new_pos(1, source.index('first') or { panic('missing first') }) + is_mut: true + }) + get := a.add_node(flat.Node{ + kind: .fn_decl + pos: token.new_pos(1, source.index('get') or { panic('missing get') }) + is_mut: true + }) + mut tc := TypeChecker.new(&a) + tc.source_texts_by_file['same_line.vh'] = source + in_first := (source.index('x U') or { panic('missing x U') }) + 2 + in_receiver := (source.index('[T]') or { panic('missing [T]') }) + 1 + assert (tc.vls_decl_at(1, in_first) or { flat.NodeId(-1) }) == first + assert (tc.vls_decl_at(1, in_receiver) or { flat.NodeId(-1) }) == get +} diff --git a/vlib/v/types/vls_query.v b/vlib/v/types/vls_query.v index 9a651b3c4f6bba..5f59aaf9ff5479 100644 --- a/vlib/v/types/vls_query.v +++ b/vlib/v/types/vls_query.v @@ -249,8 +249,9 @@ fn (tc &TypeChecker) vls_type_param_definition_at(file_id int, offset int, word } // vls_decl_at returns the declaration of a function or a type of `file_id` -// whose signature or body contains `offset`. +// whose receiver, signature or body contains `offset`. fn (tc &TypeChecker) vls_decl_at(file_id int, offset int) ?flat.NodeId { + source := tc.vls_source(file_id) mut best := -1 mut best_start := -1 for idx in tc.a.user_code_start .. tc.a.nodes.len { @@ -259,7 +260,7 @@ fn (tc &TypeChecker) vls_decl_at(file_id int, offset int) ?flat.NodeId { || node.kind !in [.fn_decl, .struct_decl, .interface_decl, .type_decl] { continue } - start := int(node.pos.offset) + start := vls_decl_start(source, node) if start <= offset && start > best_start { best = idx best_start = start @@ -271,6 +272,37 @@ fn (tc &TypeChecker) vls_decl_at(file_id int, offset int) ?flat.NodeId { return flat.NodeId(best) } +// vls_decl_start is where the declaration `node` of `source` starts: the +// receiver of a method, `(b Box[T])` of `fn (b Box[T]) get() T`, which declares +// type parameters of the method too, or else its node, at its name. +fn vls_decl_start(source string, node flat.Node) int { + name := int(node.pos.offset) + if node.kind != .fn_decl || name <= 0 || name > source.len { + return name + } + mut i := name - 1 + for i >= 0 && source[i] in [` `, `\t`, `\r`, `\n`] { + i-- + } + // Only the `)` of a receiver comes right before the name of a function. + if i < 0 || source[i] != `)` { + return name + } + mut depth := 0 + for i >= 0 { + if source[i] == `)` { + depth++ + } else if source[i] == `(` { + depth-- + if depth == 0 { + return i + } + } + i-- + } + return name +} + // vls_decl_contains_offset bounds a declaration whose node only spans its name. // Signatures and brace-delimited bodies remain in scope; following declarations do not. fn (tc &TypeChecker) vls_decl_contains_offset(decl flat.Node, offset int) bool {